Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ applications.
| Previous | Integration hardening and outcome validation ([v0.6.0](https://github.com/akoita/draft-loop/releases/tag/v0.6.0)) | Released; validation failed | Preserve a reproducible integrated baseline without overstating application readiness | Failed representative result carried into v0.7; see [stage evidence](stage-evidence-v0.6.0.md) |
| Previous | Evidence-backed CV drafting (v0.7 program) | [Released alpha.5 checkpoint](stage-evidence-v0.7.0-alpha.5.md); implementation history carried forward; outcome not validated | Produce a complete factual, source-traceable application draft | v0.8 candidate evidence now covers the bounded drafting and review vertical |
| Previous | Usable CV MVP ([v0.8.0-alpha.1](https://github.com/akoita/draft-loop/releases/tag/v0.8.0-alpha.1)) | [Released alpha](stage-evidence-v0.8.0-alpha.1.md); 17/17 issues closed; representative outcome not recorded | Produce one complete, factual, reviewed, human-approved, ATS-readable CV | Representative outcome evidence remains without overstating DOCX visual coverage |
| Now | Workflow parity and release ([milestone v0.9.0](https://github.com/akoita/draft-loop/milestone/4)) | [Twenty-five observations across two consented cases are indeterminate](consented-pilot-v0.9.md); parity not validated | Demonstrate the complete application-grade workflow and publish evidence | #350 informed corrections #351/#352; later attempts repeatedly failed before a draft. #376 corrects per-generation cap accounting, #380 adds fixed result diagnostics, and #393/#395 add private category capture. #397 established terminal reason `api_error`, and #398 added its bounded statusless classification. #401 then exhausted three author attempts on local factual and coverage validation without an artifact or review. Any later live observation requires fresh authorization. #75/#250 remain blocked |
| Now | Workflow parity and release ([milestone v0.9.0](https://github.com/akoita/draft-loop/milestone/4)) | [Twenty-five observations across two consented cases are indeterminate](consented-pilot-v0.9.md); parity not validated | Demonstrate the complete application-grade workflow and publish evidence | #350 informed corrections #351/#352; later attempts repeatedly failed before a draft. #376 corrects per-generation cap accounting, #380 adds fixed result diagnostics, and #393/#395 add private category capture. #397 established terminal reason `api_error`, and #398 added its bounded statusless classification. #401 then exhausted three author attempts on local factual and coverage validation without an artifact or review. #389 keeps application-generated mutation timestamps monotonic while retaining storage rollback rejection. Any later live observation requires fresh authorization. #75/#250 remain blocked |
| Later | Retrieval and provider quality | Integrated lexical baseline; partial components | Improve evidence selection and dependable live runs | Vector/hybrid comparison, cancellation, and provider recovery in the packaged path |
| Later | Broader real-application pilot | Implemented harness; not outcome-validated | Test factuality, quality, and effort across more cases | Consented cases, calibrated measures, and recorded limitations |
| Later | Production-ready beta | Partial implementation; not production-validated | Distribute a safe, dependable desktop application | Signed installers, safe migrations, recovery, accessibility, and platform evidence |
Expand Down Expand Up @@ -919,6 +919,7 @@ issues retain implementation chronology.

| Date | Decision | Product implication |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-09-15 | Made candidate-knowledge application timestamps monotonic under #389. | A deterministic backward-clock case reproduced the captured `StorageValidationError`. Candidate-knowledge services now retain their latest valid generated timestamp across wall-clock regressions, while storage anti-rollback validation and invalid-timestamp rejection remain unchanged. |
| 2026-09-15 | Recorded #401 as an indeterminate twenty-fifth matched-backend observation. | Both user-session authentication probes passed, but three Anthropic author responses failed local factual and substantive-coverage validation before an artifact or critic review. The run did not exercise #398's API-error branch. Authorization is exhausted, and #75/#250 remain blocked. |
| 2026-09-15 | Classified statusless structured Claude API errors under #398. | The documented `api_error` terminal reason and error-result `success` subtype now produce fixed diagnostics. Without a finite numeric status, the failure is transient and retryable only within existing orchestration caps; numeric statuses retain precedence, provider prose remains excluded, and no live attempt is authorized. |
| 2026-09-15 | Recorded #397 as an indeterminate twenty-fourth matched-backend observation. | Both authentication probes passed, but one Anthropic author attempt failed non-retryably with `unknown`. Private bounded capture established terminal reason `api_error` but no subtype or underlying cause; no artifact or critic review occurred. Authorization is exhausted, #398 owns classification, and #75/#250 remain blocked. |
Expand Down
10 changes: 10 additions & 0 deletions packages/application/src/knowledge-base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6218,6 +6218,16 @@ describe("candidate knowledge store application service", () => {
expect(current).toEqual({ sourceId: "source-uuid", checkedAt: changedAt, status: "current" });
expect(generateId).not.toHaveBeenCalled();
expect(now).toHaveBeenCalledOnce();
now.mockReturnValue(createdAt);
now.mockClear();
await expect(
service.refreshKnowledgeSourceFromOrigin({
storeRoot,
knowledgeBaseId: "default-ckb-uuid",
sourceId: "source-uuid",
}),
).resolves.toEqual({ sourceId: "source-uuid", checkedAt: changedAt, status: "current" });
expect(now).toHaveBeenCalledOnce();
const refreshState = await service.getKnowledgeSourceRefreshState({
storeRoot,
knowledgeBaseId: "default-ckb-uuid",
Expand Down
6 changes: 3 additions & 3 deletions packages/application/src/knowledge-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
restoreCandidateKnowledgePortableBackup,
} from "@draft-loop/storage/knowledge-store";
import { StorageWriterLeaseError } from "@draft-loop/storage/writer-lease";
import { monotonicTimestamp } from "./monotonic-timestamp.js";

export type {
CandidateKnowledgeBase,
Expand Down Expand Up @@ -3856,8 +3857,7 @@ async function useWriterHandle<T>(
): Promise<T> {
return useHandle(acquire, (handle) => {
if (typeof handle.withWriterLease !== "function") {
// Narrow compatibility seam for unit-test adapters. Production handles
// always expose the coordinated writer contract.
// Narrow compatibility seam for unit-test adapters; production handles expose this contract.
return callback(handle);
}
return handle.withWriterLease(operation, () => callback(handle));
Expand All @@ -3869,7 +3869,7 @@ function resolveDependencies(
): ResolvedDependencies {
return {
generateId: dependencies.generateId ?? randomUUID,
now: dependencies.now ?? (() => new Date().toISOString()),
now: monotonicTimestamp(dependencies.now ?? (() => new Date().toISOString())),
initialize: dependencies.initialize ?? initializeCandidateKnowledgeStore,
open: dependencies.open ?? openCandidateKnowledgeStore,
exportPortableBackup:
Expand Down
28 changes: 28 additions & 0 deletions packages/application/src/monotonic-timestamp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";

import { monotonicTimestamp } from "./monotonic-timestamp.js";

describe("monotonicTimestamp", () => {
it("keeps the latest valid value when the wall clock moves backward", () => {
const values = [
"2026-09-15T01:00:00.000Z",
"2026-09-15T02:00:00.000Z",
"2026-09-15T01:30:00.000Z",
];
const now = monotonicTimestamp(() => values.shift() as string);

expect([now(), now(), now()]).toEqual([
"2026-09-15T01:00:00.000Z",
"2026-09-15T02:00:00.000Z",
"2026-09-15T02:00:00.000Z",
]);
});

it("passes invalid values through for the existing validation boundary", () => {
const values = ["2026-09-15T01:00:00.000Z", "not-a-time"];
const now = monotonicTimestamp(() => values.shift() as string);

expect(now()).toBe("2026-09-15T01:00:00.000Z");
expect(now()).toBe("not-a-time");
});
});
15 changes: 15 additions & 0 deletions packages/application/src/monotonic-timestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** Keep application-generated timestamps nondecreasing across wall-clock regressions. */
export function monotonicTimestamp(clock: () => string): () => string {
let latestTime = Number.NEGATIVE_INFINITY;
let latestValue: string | undefined;

return () => {
const value = clock();
const time = Date.parse(value);
if (!Number.isFinite(time)) return value;
if (time < latestTime && latestValue !== undefined) return latestValue;
latestTime = time;
latestValue = value;
return value;
};
}