@doxbrix/doxloop 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1145 -0
- package/README.md +422 -227
- package/assets/doxbrix-preview.css +77 -15
- package/contracts/agent-events-v1.schema.json +1 -0
- package/contracts/coverage-v1.schema.json +59 -0
- package/contracts/drift-v1.schema.json +1 -0
- package/contracts/evaluation-v1.schema.json +62 -0
- package/contracts/quality-codes-v1.json +1 -0
- package/contracts/quality-report-v1.schema.json +1 -0
- package/contracts/validation-v1.schema.json +1 -0
- package/dist/agent-failure.d.ts +36 -0
- package/dist/agent-failure.js +98 -0
- package/dist/agent-isolation.d.ts +17 -0
- package/dist/agent-isolation.js +94 -0
- package/dist/agent-log.d.ts +197 -0
- package/dist/agent-log.js +1094 -0
- package/dist/agent-process.d.ts +63 -0
- package/dist/agent-process.js +128 -0
- package/dist/agent-reply.d.ts +47 -0
- package/dist/agent-reply.js +315 -0
- package/dist/agents.d.ts +24 -5
- package/dist/agents.js +60 -9
- package/dist/api-coverage.d.ts +11 -0
- package/dist/api-coverage.js +72 -0
- package/dist/application-probe.d.ts +28 -0
- package/dist/application-probe.js +93 -0
- package/dist/application-url.d.ts +14 -0
- package/dist/application-url.js +26 -0
- package/dist/args.js +15 -0
- package/dist/artifact-deploy.d.ts +9 -0
- package/dist/artifact-deploy.js +28 -8
- package/dist/assets.d.ts +72 -0
- package/dist/assets.js +382 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +38 -9
- package/dist/author.d.ts +206 -4
- package/dist/author.js +1650 -52
- package/dist/authoring-batches.d.ts +190 -0
- package/dist/authoring-batches.js +489 -0
- package/dist/authoring-postpass.d.ts +79 -0
- package/dist/authoring-postpass.js +1289 -0
- package/dist/authoring-progress.d.ts +95 -0
- package/dist/authoring-progress.js +269 -0
- package/dist/autosync.d.ts +56 -0
- package/dist/autosync.js +442 -0
- package/dist/batch-artifacts.d.ts +81 -0
- package/dist/batch-artifacts.js +412 -0
- package/dist/batch-limits.d.ts +37 -0
- package/dist/batch-limits.js +80 -0
- package/dist/branding.d.ts +41 -0
- package/dist/branding.js +229 -0
- package/dist/bundle-upload.d.ts +26 -0
- package/dist/bundle-upload.js +99 -0
- package/dist/capture-auth.d.ts +95 -0
- package/dist/capture-auth.js +204 -0
- package/dist/capture.d.ts +48 -0
- package/dist/capture.js +20 -1
- package/dist/cli.js +839 -32
- package/dist/content-links.d.ts +10 -0
- package/dist/content-links.js +49 -0
- package/dist/contract-validation.d.ts +4 -0
- package/dist/contract-validation.js +25 -0
- package/dist/coverage-actions.d.ts +8 -0
- package/dist/coverage-actions.js +71 -0
- package/dist/coverage-resolutions.d.ts +16 -0
- package/dist/coverage-resolutions.js +58 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +288 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +122 -0
- package/dist/deploy-credentials.d.ts +7 -0
- package/dist/deploy-credentials.js +76 -0
- package/dist/deploy-targets/github-pages.d.ts +3 -0
- package/dist/deploy-targets/github-pages.js +100 -0
- package/dist/deploy-targets/index.d.ts +8 -0
- package/dist/deploy-targets/index.js +78 -0
- package/dist/deploy-targets/netlify.d.ts +3 -0
- package/dist/deploy-targets/netlify.js +49 -0
- package/dist/deploy-targets/types.d.ts +32 -0
- package/dist/deploy-targets/types.js +2 -0
- package/dist/deploy-targets/vercel.d.ts +3 -0
- package/dist/deploy-targets/vercel.js +95 -0
- package/dist/deploy.d.ts +9 -0
- package/dist/deploy.js +87 -13
- package/dist/deterministic-capture.d.ts +90 -0
- package/dist/deterministic-capture.js +435 -0
- package/dist/direct-edit.d.ts +40 -0
- package/dist/direct-edit.js +159 -0
- package/dist/docs-crawl.d.ts +86 -0
- package/dist/docs-crawl.js +536 -0
- package/dist/docs-site.d.ts +49 -0
- package/dist/docs-site.js +233 -0
- package/dist/doctor.js +8 -0
- package/dist/documentation-collections.d.ts +23 -0
- package/dist/documentation-collections.js +191 -0
- package/dist/documentation-plan.d.ts +253 -0
- package/dist/documentation-plan.js +2563 -0
- package/dist/doxbrix-build.d.ts +19 -0
- package/dist/doxbrix-build.js +167 -0
- package/dist/doxbrix-markdown.d.ts +9 -0
- package/dist/doxbrix-markdown.js +115 -14
- package/dist/drift.d.ts +10 -0
- package/dist/drift.js +164 -0
- package/dist/evaluation.d.ts +46 -0
- package/dist/evaluation.js +113 -0
- package/dist/evidence-pack.d.ts +47 -0
- package/dist/evidence-pack.js +358 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.js +175 -0
- package/dist/fs.d.ts +8 -2
- package/dist/fs.js +41 -11
- package/dist/generator-api.d.ts +89 -0
- package/dist/generator-preflight.d.ts +27 -0
- package/dist/generator-preflight.js +105 -0
- package/dist/generator-runtime.d.ts +7 -0
- package/dist/generator-runtime.js +17 -1
- package/dist/generators.d.ts +24 -2
- package/dist/generators.js +54 -1
- package/dist/git-delivery.d.ts +17 -0
- package/dist/git-delivery.js +123 -0
- package/dist/globs.d.ts +16 -0
- package/dist/globs.js +65 -0
- package/dist/glossary.d.ts +26 -0
- package/dist/glossary.js +179 -0
- package/dist/history.d.ts +106 -0
- package/dist/history.js +600 -0
- package/dist/html-markdown.d.ts +46 -0
- package/dist/html-markdown.js +423 -0
- package/dist/interactive.js +3 -0
- package/dist/job-events.d.ts +74 -0
- package/dist/job-events.js +377 -0
- package/dist/keep-awake.d.ts +50 -0
- package/dist/keep-awake.js +123 -0
- package/dist/local-source-snapshot.d.ts +20 -0
- package/dist/local-source-snapshot.js +61 -0
- package/dist/mintlify-detect.d.ts +3 -0
- package/dist/mintlify-detect.js +18 -0
- package/dist/mintlify-import.d.ts +75 -0
- package/dist/mintlify-import.js +190 -0
- package/dist/navigation.d.ts +98 -0
- package/dist/navigation.js +310 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.js +439 -0
- package/dist/page-editor-bridge.d.ts +3 -0
- package/dist/page-editor-bridge.js +109 -0
- package/dist/page-editor-preview.d.ts +10 -0
- package/dist/page-editor-preview.js +55 -0
- package/dist/page-extension.d.ts +9 -0
- package/dist/page-extension.js +15 -0
- package/dist/page-metadata.d.ts +28 -0
- package/dist/page-metadata.js +166 -0
- package/dist/page-operations.d.ts +34 -0
- package/dist/page-operations.js +215 -0
- package/dist/page-routes.d.ts +4 -0
- package/dist/page-routes.js +61 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.js +184 -0
- package/dist/plan-generator.d.ts +3 -0
- package/dist/plan-generator.js +21 -0
- package/dist/plan-navigation.d.ts +11 -0
- package/dist/plan-navigation.js +30 -0
- package/dist/planning-captures.d.ts +20 -0
- package/dist/planning-captures.js +143 -0
- package/dist/planning-research.d.ts +135 -0
- package/dist/planning-research.js +472 -0
- package/dist/planning-triage.d.ts +23 -0
- package/dist/planning-triage.js +131 -0
- package/dist/preview.d.ts +24 -0
- package/dist/preview.js +280 -29
- package/dist/project-detect.d.ts +36 -0
- package/dist/project-detect.js +251 -0
- package/dist/project-import.d.ts +54 -0
- package/dist/project-import.js +157 -0
- package/dist/project-lock.d.ts +6 -0
- package/dist/project-lock.js +96 -0
- package/dist/project-registry.d.ts +25 -0
- package/dist/project-registry.js +79 -0
- package/dist/project.d.ts +35 -3
- package/dist/project.js +388 -38
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +25 -0
- package/dist/proposal-replay.d.ts +32 -0
- package/dist/proposal-replay.js +99 -0
- package/dist/quality-claims.d.ts +8 -0
- package/dist/quality-claims.js +168 -0
- package/dist/quality-config.d.ts +5 -0
- package/dist/quality-config.js +84 -0
- package/dist/quality-contract.d.ts +37 -0
- package/dist/quality-contract.js +46 -0
- package/dist/quality-examples.d.ts +4 -0
- package/dist/quality-examples.js +233 -0
- package/dist/quality-gates.d.ts +16 -0
- package/dist/quality-gates.js +192 -0
- package/dist/quality-links.d.ts +7 -0
- package/dist/quality-links.js +149 -0
- package/dist/quality-lint.d.ts +6 -0
- package/dist/quality-lint.js +124 -0
- package/dist/quality-rendered.d.ts +11 -0
- package/dist/quality-rendered.js +222 -0
- package/dist/quality-schema.d.ts +3 -0
- package/dist/quality-schema.js +71 -0
- package/dist/release-notes.d.ts +44 -0
- package/dist/release-notes.js +183 -0
- package/dist/remote-monitor.d.ts +16 -0
- package/dist/remote-monitor.js +74 -0
- package/dist/remote-source.d.ts +34 -0
- package/dist/remote-source.js +426 -0
- package/dist/review-diff.d.ts +82 -0
- package/dist/review-diff.js +400 -0
- package/dist/review-learning.d.ts +11 -0
- package/dist/review-learning.js +60 -0
- package/dist/review-render.d.ts +38 -0
- package/dist/review-render.js +224 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.js +89 -0
- package/dist/review-ui.d.ts +14 -0
- package/dist/review-ui.js +1248 -0
- package/dist/schedule.d.ts +78 -0
- package/dist/schedule.js +480 -0
- package/dist/screen-capture-provider.d.ts +52 -0
- package/dist/screen-capture-provider.js +218 -0
- package/dist/screenshot-workflow.d.ts +167 -0
- package/dist/screenshot-workflow.js +1237 -0
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +95 -7
- package/dist/site-export.d.ts +18 -0
- package/dist/site-export.js +87 -0
- package/dist/source-connectors.d.ts +33 -0
- package/dist/source-connectors.js +268 -0
- package/dist/source-discovery.d.ts +132 -0
- package/dist/source-discovery.js +823 -0
- package/dist/source-intelligence.d.ts +9 -0
- package/dist/source-intelligence.js +306 -0
- package/dist/sync-review.d.ts +28 -0
- package/dist/sync-review.js +264 -0
- package/dist/sync-runs.d.ts +192 -0
- package/dist/sync-runs.js +2244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +298 -32
- package/dist/text-diff.d.ts +9 -0
- package/dist/text-diff.js +59 -0
- package/dist/types.d.ts +946 -1
- package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
- package/dist/ui/assets/index-BHBYU2aG.css +1 -0
- package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui-server.d.ts +83 -0
- package/dist/ui-server.js +3532 -0
- package/dist/usage-budget.d.ts +28 -0
- package/dist/usage-budget.js +90 -0
- package/dist/validation.d.ts +25 -1
- package/dist/validation.js +312 -26
- package/dist/workspace-tools.d.ts +54 -0
- package/dist/workspace-tools.js +123 -0
- package/docs/agent-compatibility.md +49 -32
- package/docs/ci-and-automation.md +105 -66
- package/docs/doxbrix-http-api.md +8 -1
- package/docs/existing-documentation.md +80 -0
- package/docs/generation-performance.md +108 -0
- package/docs/generator-authoring.md +68 -5
- package/docs/generator-selection.md +50 -13
- package/docs/mintlify-import.md +71 -0
- package/docs/openapi-security.md +25 -0
- package/docs/project-format.md +331 -36
- package/docs/release-quality.md +158 -0
- package/docs/releasing.md +72 -0
- package/docs/review-workflows.md +51 -0
- package/docs/security-model.md +102 -39
- package/docs/troubleshooting.md +226 -93
- package/package.json +46 -15
- package/scripts/test-auto-screenshot.mjs +172 -0
- package/skills/doxloop-authoring/SKILL.md +242 -361
- package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
- package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
- package/skills/doxloop-authoring/references/page-depth.md +169 -0
- package/skills/doxloop-authoring/references/project-format.md +106 -7
- package/skills/doxloop-authoring/references/quality.md +10 -0
- package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
- package/skills/doxloop-authoring/references/screenshots.md +149 -235
- package/skills/doxloop-authoring/references/workflows.md +76 -0
- package/skills/doxloop-doxbrix/SKILL.md +38 -22
- package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
- package/skills/doxloop-doxbrix/references/components.md +30 -3
- package/skills/doxloop-doxbrix/references/manifest.md +3 -2
- package/vendor/doxbrix-import/LICENSE +202 -0
- package/vendor/doxbrix-import/README.md +17 -0
- package/vendor/doxbrix-import/UPSTREAM.json +47 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
- package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
- package/vendor/doxbrix-import/dist/docs/import.js +147 -0
- package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
- package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
- package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
- package/vendor/doxbrix-import/dist/docs/project.js +77 -0
- package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
- package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
- package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
- package/vendor/doxbrix-import/dist/importer.js +1567 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
- package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
- package/vendor/doxbrix-import/dist/safe-path.js +47 -0
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/artifact-deploy.d.ts.map +0 -1
- package/dist/artifact-deploy.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/author.d.ts.map +0 -1
- package/dist/author.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/deploy.d.ts.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/deployment-visibility.d.ts.map +0 -1
- package/dist/deployment-visibility.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/doxbrix-markdown.d.ts.map +0 -1
- package/dist/doxbrix-markdown.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/generator-api.d.ts.map +0 -1
- package/dist/generator-api.js.map +0 -1
- package/dist/generator-manager.d.ts.map +0 -1
- package/dist/generator-manager.js.map +0 -1
- package/dist/generator-runtime.d.ts.map +0 -1
- package/dist/generator-runtime.js.map +0 -1
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/preview.d.ts.map +0 -1
- package/dist/preview.js.map +0 -1
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/history.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { openHistory, withHistory } from './db.js';
|
|
5
|
+
import { readEvidenceMap } from './evidence.js';
|
|
6
|
+
import { pathExists } from './fs.js';
|
|
7
|
+
import { loadPages, loadProject } from './project.js';
|
|
8
|
+
import { lineHunks } from './text-diff.js';
|
|
9
|
+
/** Open a request row before the agent starts, so an interrupted run is still visible. */
|
|
10
|
+
export async function startRequest(root, input) {
|
|
11
|
+
const id = `req-${randomUUID().slice(0, 12)}`;
|
|
12
|
+
const recorded = await withHistory(root, (database) => {
|
|
13
|
+
database
|
|
14
|
+
.prepare(`INSERT INTO requests (id, created_at, kind, trigger, request_text, agent, model,
|
|
15
|
+
reasoning_effort, status)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running')`)
|
|
17
|
+
.run(id, now(), input.kind, input.trigger ?? 'manual', text(input.requestText), text(input.agent), text(input.model), text(input.reasoningEffort));
|
|
18
|
+
return id;
|
|
19
|
+
});
|
|
20
|
+
return recorded;
|
|
21
|
+
}
|
|
22
|
+
export async function finishRequest(root, id, input) {
|
|
23
|
+
if (!id)
|
|
24
|
+
return;
|
|
25
|
+
await withHistory(root, (database) => {
|
|
26
|
+
database
|
|
27
|
+
.prepare(`UPDATE requests
|
|
28
|
+
SET finished_at = ?,
|
|
29
|
+
duration_ms = CAST((julianday(?) - julianday(created_at)) * 86400000 AS INTEGER),
|
|
30
|
+
status = ?,
|
|
31
|
+
pages_changed = COALESCE(?, pages_changed),
|
|
32
|
+
lines_added = COALESCE(?, lines_added),
|
|
33
|
+
lines_removed = COALESCE(?, lines_removed),
|
|
34
|
+
validation_pages = ?,
|
|
35
|
+
validation_errors = ?,
|
|
36
|
+
validation_warnings = ?,
|
|
37
|
+
error_message = ?,
|
|
38
|
+
input_tokens = COALESCE(?, input_tokens),
|
|
39
|
+
output_tokens = COALESCE(?, output_tokens),
|
|
40
|
+
cache_read_tokens = COALESCE(?, cache_read_tokens),
|
|
41
|
+
cache_creation_tokens = COALESCE(?, cache_creation_tokens),
|
|
42
|
+
cost_usd = COALESCE(?, cost_usd),
|
|
43
|
+
agent_turns = COALESCE(?, agent_turns),
|
|
44
|
+
agent_sessions = COALESCE(?, agent_sessions),
|
|
45
|
+
max_context_tokens = COALESCE(?, max_context_tokens)
|
|
46
|
+
WHERE id = ?`)
|
|
47
|
+
.run(now(), now(), input.status, input.pagesChanged ?? null, input.linesAdded ?? null, input.linesRemoved ?? null, input.validation?.pages ?? null, input.validation?.errors ?? null, input.validation?.warnings ?? null, text(input.error), ...usageColumns(input.usage), id);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Capture page content so a direct authoring run can be measured afterwards.
|
|
52
|
+
* Held in memory rather than read back from the page registry: the UI server
|
|
53
|
+
* backfills that registry on startup, which would otherwise claim the pages a
|
|
54
|
+
* still-running agent is in the middle of writing.
|
|
55
|
+
*/
|
|
56
|
+
export async function snapshotPages(root, project) {
|
|
57
|
+
const snapshot = new Map();
|
|
58
|
+
try {
|
|
59
|
+
const resolved = project ?? (await loadProject(root));
|
|
60
|
+
for (const absolute of await loadPages(root, resolved)) {
|
|
61
|
+
snapshot.set(portable(relative(root, absolute)), await readFile(absolute, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// A project with no content directory yet simply starts from nothing.
|
|
66
|
+
}
|
|
67
|
+
return snapshot;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Record what an authoring run changed. `create` and `update` write straight to
|
|
71
|
+
* disk instead of producing a proposal, so the diff against the pre-run
|
|
72
|
+
* snapshot is the only account of which pages the request touched.
|
|
73
|
+
*/
|
|
74
|
+
export async function recordAuthoredPages(root, requestId, before, project) {
|
|
75
|
+
const result = { paths: new Set(), linesAdded: 0, linesRemoved: 0 };
|
|
76
|
+
const after = await snapshotPages(root, project);
|
|
77
|
+
const changes = [];
|
|
78
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
79
|
+
for (const path of [...paths].sort()) {
|
|
80
|
+
const previous = before.get(path);
|
|
81
|
+
const current = after.get(path);
|
|
82
|
+
if (previous === current)
|
|
83
|
+
continue;
|
|
84
|
+
let added = 0;
|
|
85
|
+
let removed = 0;
|
|
86
|
+
for (const hunk of lineHunks(previous ?? '', current ?? '', path)) {
|
|
87
|
+
added += hunk.newLines.length;
|
|
88
|
+
removed += hunk.oldLines.length;
|
|
89
|
+
}
|
|
90
|
+
result.paths.add(path);
|
|
91
|
+
result.linesAdded += added;
|
|
92
|
+
result.linesRemoved += removed;
|
|
93
|
+
changes.push({
|
|
94
|
+
path,
|
|
95
|
+
kind: current === undefined ? 'deleted' : previous === undefined ? 'added' : 'modified',
|
|
96
|
+
title: text(frontmatterTitle(current ?? previous ?? '')),
|
|
97
|
+
added,
|
|
98
|
+
removed,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (!requestId || changes.length === 0)
|
|
102
|
+
return result;
|
|
103
|
+
await withHistory(root, (database) => {
|
|
104
|
+
const timestamp = now();
|
|
105
|
+
// Authoring writes straight to the working tree, so every page it touched is
|
|
106
|
+
// already in effect — there is no pending decision to make later.
|
|
107
|
+
const upsert = database.prepare(`INSERT INTO request_pages (request_id, path, title, change_kind, category, decision,
|
|
108
|
+
decided_at, lines_added, lines_removed)
|
|
109
|
+
VALUES (?, ?, ?, ?, 'page', 'accepted', ?, ?, ?)
|
|
110
|
+
ON CONFLICT (request_id, path) DO UPDATE SET
|
|
111
|
+
title = excluded.title,
|
|
112
|
+
change_kind = excluded.change_kind,
|
|
113
|
+
lines_added = excluded.lines_added,
|
|
114
|
+
lines_removed = excluded.lines_removed`);
|
|
115
|
+
for (const change of changes) {
|
|
116
|
+
upsert.run(requestId, change.path, change.title, change.kind, timestamp, change.added, change.removed);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Mirror a proposal into history. Called on every status change, so the row
|
|
123
|
+
* survives even when the run directory is later pruned.
|
|
124
|
+
*/
|
|
125
|
+
export async function recordSyncRun(root, run, context = {}) {
|
|
126
|
+
await withHistory(root, (database) => {
|
|
127
|
+
const counts = lineCounts(run.changes);
|
|
128
|
+
const finishedAt = run.appliedAt ?? run.rejectedAt ?? run.completedAt ?? null;
|
|
129
|
+
database
|
|
130
|
+
.prepare(`INSERT INTO requests (id, created_at, finished_at, duration_ms, kind, trigger,
|
|
131
|
+
request_text, agent, model, reasoning_effort, status, pages_changed, lines_added,
|
|
132
|
+
lines_removed, validation_pages, validation_errors, validation_warnings,
|
|
133
|
+
source_summary, stale_pages_count, error_message, run_dir, input_tokens,
|
|
134
|
+
output_tokens, cache_read_tokens, cache_creation_tokens, cost_usd, agent_turns,
|
|
135
|
+
agent_sessions, max_context_tokens)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
138
|
+
finished_at = excluded.finished_at,
|
|
139
|
+
duration_ms = excluded.duration_ms,
|
|
140
|
+
kind = excluded.kind,
|
|
141
|
+
status = excluded.status,
|
|
142
|
+
pages_changed = excluded.pages_changed,
|
|
143
|
+
lines_added = excluded.lines_added,
|
|
144
|
+
lines_removed = excluded.lines_removed,
|
|
145
|
+
validation_pages = excluded.validation_pages,
|
|
146
|
+
validation_errors = excluded.validation_errors,
|
|
147
|
+
validation_warnings = excluded.validation_warnings,
|
|
148
|
+
source_summary = excluded.source_summary,
|
|
149
|
+
stale_pages_count = excluded.stale_pages_count,
|
|
150
|
+
error_message = excluded.error_message,
|
|
151
|
+
request_text = COALESCE(excluded.request_text, requests.request_text),
|
|
152
|
+
agent = COALESCE(excluded.agent, requests.agent),
|
|
153
|
+
model = COALESCE(excluded.model, requests.model),
|
|
154
|
+
input_tokens = COALESCE(excluded.input_tokens, requests.input_tokens),
|
|
155
|
+
output_tokens = COALESCE(excluded.output_tokens, requests.output_tokens),
|
|
156
|
+
cache_read_tokens = COALESCE(excluded.cache_read_tokens, requests.cache_read_tokens),
|
|
157
|
+
cache_creation_tokens = COALESCE(excluded.cache_creation_tokens, requests.cache_creation_tokens),
|
|
158
|
+
cost_usd = COALESCE(excluded.cost_usd, requests.cost_usd),
|
|
159
|
+
agent_turns = COALESCE(excluded.agent_turns, requests.agent_turns),
|
|
160
|
+
agent_sessions = COALESCE(excluded.agent_sessions, requests.agent_sessions),
|
|
161
|
+
max_context_tokens = COALESCE(excluded.max_context_tokens, requests.max_context_tokens)`)
|
|
162
|
+
.run(run.id, run.createdAt, finishedAt, finishedAt ? elapsed(run.createdAt, finishedAt) : null, run.editRequest ? 'edit' : run.authoringMode ?? 'update', run.trigger, text(run.editRequest?.followUps.at(-1)?.instruction ?? run.editRequest?.instruction ?? context.requestText), text(context.agent), text(context.model), text(context.reasoningEffort), run.status, run.changes.filter((change) => change.category === 'page').length, counts.added, counts.removed, run.validation?.pages ?? null, run.validation?.errors ?? null, run.validation?.warnings ?? null, text(run.sourceSummary), run.stalePages.length, text(run.error), text(context.runDir), ...usageColumns(run.usage));
|
|
163
|
+
const upsertPage = database.prepare(`INSERT INTO request_pages (request_id, path, title, change_kind, category, decision,
|
|
164
|
+
decided_at, lines_added, lines_removed)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
166
|
+
ON CONFLICT (request_id, path) DO UPDATE SET
|
|
167
|
+
title = COALESCE(excluded.title, request_pages.title),
|
|
168
|
+
change_kind = excluded.change_kind,
|
|
169
|
+
category = excluded.category,
|
|
170
|
+
decision = excluded.decision,
|
|
171
|
+
decided_at = excluded.decided_at,
|
|
172
|
+
lines_added = excluded.lines_added,
|
|
173
|
+
lines_removed = excluded.lines_removed`);
|
|
174
|
+
if (run.editRequest) {
|
|
175
|
+
for (const path of run.editRequest.paths) {
|
|
176
|
+
if (run.changes.some((change) => change.path === path))
|
|
177
|
+
continue;
|
|
178
|
+
upsertPage.run(run.id, path, null, 'modified', 'page', 'pending', null, 0, 0);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const change of run.changes) {
|
|
182
|
+
const decision = changeDecision(change);
|
|
183
|
+
const changeCounts = lineCounts([change]);
|
|
184
|
+
upsertPage.run(run.id, change.path, text(change.title), change.kind, change.category, decision.state, decision.at, changeCounts.added, changeCounts.removed);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** Append the baselines a run was written against. Ignores repeats. */
|
|
189
|
+
export async function recordSourceSyncs(root, state, requestId) {
|
|
190
|
+
if (!state)
|
|
191
|
+
return;
|
|
192
|
+
await withHistory(root, (database) => {
|
|
193
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO source_syncs (source_name, commit_hash, fingerprint, recorded_at,
|
|
194
|
+
request_id)
|
|
195
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
196
|
+
for (const [name, record] of Object.entries(state.sources)) {
|
|
197
|
+
insert.run(name, record.commit ?? '', record.contentFingerprint ?? '', record.recordedAt, text(requestId));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export async function recordDeployment(root, input) {
|
|
202
|
+
await withHistory(root, (database) => {
|
|
203
|
+
const finishedAt = now();
|
|
204
|
+
database
|
|
205
|
+
.prepare(`INSERT INTO deployments (started_at, finished_at, duration_ms, target, name, slug,
|
|
206
|
+
visibility, url, status, pages_count, media_count, bytes, pages_created,
|
|
207
|
+
pages_updated, pages_deleted, error_message)
|
|
208
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
209
|
+
.run(input.startedAt, finishedAt, elapsed(input.startedAt, finishedAt), input.target, text(input.name), text(input.slug), text(input.visibility), text(input.url), input.status, input.pagesCount ?? null, input.mediaCount ?? null, input.bytes ?? null, input.pagesCreated ?? null, input.pagesUpdated ?? null, input.pagesDeleted ?? null, text(input.error));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Reconcile the page registry with what is on disk. Content hashes make edits
|
|
214
|
+
* that bypassed Doxloop visible instead of silently absent from history.
|
|
215
|
+
*/
|
|
216
|
+
export async function syncPageRegistry(root, project, requestId, authored) {
|
|
217
|
+
const database = await openHistory(root);
|
|
218
|
+
if (!database)
|
|
219
|
+
return;
|
|
220
|
+
try {
|
|
221
|
+
const resolved = project ?? (await loadProject(root));
|
|
222
|
+
const contentRoot = join(root, resolved.contentDir);
|
|
223
|
+
const absolutePaths = await loadPages(root, resolved);
|
|
224
|
+
const evidence = await readEvidenceMap(root);
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
const timestamp = now();
|
|
227
|
+
for (const absolute of absolutePaths) {
|
|
228
|
+
const path = portable(relative(root, absolute));
|
|
229
|
+
seen.add(path);
|
|
230
|
+
const content = await readFile(absolute, 'utf8');
|
|
231
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
232
|
+
const title = frontmatterTitle(content) ?? portable(relative(contentRoot, absolute));
|
|
233
|
+
const confidence = evidence?.pages[path]?.confidence;
|
|
234
|
+
const existing = database
|
|
235
|
+
.prepare('SELECT content_hash FROM pages WHERE path = ?')
|
|
236
|
+
.get(path);
|
|
237
|
+
if (existing?.content_hash === hash) {
|
|
238
|
+
database
|
|
239
|
+
.prepare('UPDATE pages SET title = ?, evidence_confidence = ?, status = ? WHERE path = ?')
|
|
240
|
+
.run(title, text(confidence), 'active', path);
|
|
241
|
+
// A registry refresh may have recorded this content first — the UI server
|
|
242
|
+
// backfills on startup, mid-run. The hash then matches even though this
|
|
243
|
+
// request is what wrote the page, so attribution has to be set explicitly.
|
|
244
|
+
if (requestId && authored?.has(path)) {
|
|
245
|
+
database
|
|
246
|
+
.prepare('UPDATE pages SET last_request_id = ? WHERE path = ?')
|
|
247
|
+
.run(requestId, path);
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
database
|
|
252
|
+
.prepare(`INSERT INTO pages (path, title, created_at, updated_at, last_request_id, content_hash,
|
|
253
|
+
change_count, evidence_confidence, status)
|
|
254
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, 'active')
|
|
255
|
+
ON CONFLICT (path) DO UPDATE SET
|
|
256
|
+
title = excluded.title,
|
|
257
|
+
updated_at = excluded.updated_at,
|
|
258
|
+
last_request_id = excluded.last_request_id,
|
|
259
|
+
content_hash = excluded.content_hash,
|
|
260
|
+
change_count = pages.change_count + 1,
|
|
261
|
+
evidence_confidence = excluded.evidence_confidence,
|
|
262
|
+
status = 'active'`)
|
|
263
|
+
.run(path, title, timestamp, timestamp, text(requestId), hash, text(confidence));
|
|
264
|
+
}
|
|
265
|
+
const known = database.prepare("SELECT path FROM pages WHERE status = 'active'").all();
|
|
266
|
+
for (const row of known) {
|
|
267
|
+
if (seen.has(row.path))
|
|
268
|
+
continue;
|
|
269
|
+
database
|
|
270
|
+
.prepare("UPDATE pages SET status = 'deleted', updated_at = ?, last_request_id = ? WHERE path = ?")
|
|
271
|
+
.run(timestamp, text(requestId), row.path);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// A registry refresh is best effort; documentation commands must not fail on it.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export async function listRequests(root, limit = 20) {
|
|
279
|
+
const rows = await withHistory(root, (database) => database
|
|
280
|
+
.prepare(`SELECT id, created_at, finished_at, duration_ms, kind, trigger, request_text, agent,
|
|
281
|
+
model, status, pages_changed, lines_added, lines_removed, validation_errors,
|
|
282
|
+
validation_warnings, source_summary, error_message, input_tokens, output_tokens,
|
|
283
|
+
cache_read_tokens, cache_creation_tokens, cost_usd, agent_turns, agent_sessions,
|
|
284
|
+
max_context_tokens
|
|
285
|
+
FROM requests
|
|
286
|
+
ORDER BY created_at DESC
|
|
287
|
+
LIMIT ?`)
|
|
288
|
+
.all(limit));
|
|
289
|
+
return (rows ?? []).map(toRequest);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The pages each request touched, keyed by request id. Attached to a request
|
|
293
|
+
* list so the workspace can show what changed without a second round trip.
|
|
294
|
+
*/
|
|
295
|
+
export async function requestPages(root, requestIds) {
|
|
296
|
+
if (requestIds.length === 0)
|
|
297
|
+
return {};
|
|
298
|
+
const rows = await withHistory(root, (database) => database
|
|
299
|
+
.prepare(`SELECT request_id, path, title, change_kind, decision, lines_added, lines_removed
|
|
300
|
+
FROM request_pages
|
|
301
|
+
WHERE category = 'page'
|
|
302
|
+
AND request_id IN (${requestIds.map(() => '?').join(', ')})
|
|
303
|
+
ORDER BY path`)
|
|
304
|
+
.all(...requestIds));
|
|
305
|
+
const grouped = {};
|
|
306
|
+
for (const row of rows ?? []) {
|
|
307
|
+
const record = row;
|
|
308
|
+
const id = String(record.request_id);
|
|
309
|
+
const pages = grouped[id] ?? (grouped[id] = []);
|
|
310
|
+
pages.push({
|
|
311
|
+
path: String(record.path),
|
|
312
|
+
title: optional(record.title),
|
|
313
|
+
changeKind: String(record.change_kind),
|
|
314
|
+
decision: String(record.decision),
|
|
315
|
+
linesAdded: Number(record.lines_added ?? 0),
|
|
316
|
+
linesRemoved: Number(record.lines_removed ?? 0),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return grouped;
|
|
320
|
+
}
|
|
321
|
+
export async function pageHistory(root, path, limit = 20) {
|
|
322
|
+
const rows = await withHistory(root, (database) => database
|
|
323
|
+
.prepare(`SELECT rp.request_id, rp.path, rp.title, rp.change_kind, rp.decision, rp.decided_at,
|
|
324
|
+
rp.lines_added, rp.lines_removed, r.created_at, r.request_text, r.agent, r.status
|
|
325
|
+
FROM request_pages rp
|
|
326
|
+
JOIN requests r ON r.id = rp.request_id
|
|
327
|
+
WHERE rp.path = ?
|
|
328
|
+
ORDER BY r.created_at DESC
|
|
329
|
+
LIMIT ?`)
|
|
330
|
+
.all(path, limit));
|
|
331
|
+
return (rows ?? []).map((row) => {
|
|
332
|
+
const record = row;
|
|
333
|
+
return {
|
|
334
|
+
requestId: String(record.request_id),
|
|
335
|
+
path: String(record.path),
|
|
336
|
+
title: optional(record.title),
|
|
337
|
+
changeKind: String(record.change_kind),
|
|
338
|
+
decision: String(record.decision),
|
|
339
|
+
decidedAt: optional(record.decided_at),
|
|
340
|
+
linesAdded: Number(record.lines_added ?? 0),
|
|
341
|
+
linesRemoved: Number(record.lines_removed ?? 0),
|
|
342
|
+
requestedAt: String(record.created_at),
|
|
343
|
+
requestText: optional(record.request_text),
|
|
344
|
+
agent: optional(record.agent),
|
|
345
|
+
requestStatus: String(record.status),
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
export async function listPages(root) {
|
|
350
|
+
const rows = await withHistory(root, (database) => database
|
|
351
|
+
.prepare(`SELECT path, title, created_at, updated_at, change_count, evidence_confidence, status
|
|
352
|
+
FROM pages
|
|
353
|
+
ORDER BY updated_at DESC`)
|
|
354
|
+
.all());
|
|
355
|
+
return (rows ?? []).map((row) => {
|
|
356
|
+
const record = row;
|
|
357
|
+
return {
|
|
358
|
+
path: String(record.path),
|
|
359
|
+
title: optional(record.title),
|
|
360
|
+
createdAt: String(record.created_at),
|
|
361
|
+
updatedAt: String(record.updated_at),
|
|
362
|
+
changeCount: Number(record.change_count ?? 0),
|
|
363
|
+
evidenceConfidence: optional(record.evidence_confidence),
|
|
364
|
+
status: String(record.status),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
export async function listDeployments(root, limit = 20) {
|
|
369
|
+
const rows = await withHistory(root, (database) => database
|
|
370
|
+
.prepare(`SELECT started_at, finished_at, duration_ms, target, name, slug, visibility, url, status,
|
|
371
|
+
pages_count, pages_created, pages_updated, pages_deleted, error_message
|
|
372
|
+
FROM deployments
|
|
373
|
+
ORDER BY started_at DESC
|
|
374
|
+
LIMIT ?`)
|
|
375
|
+
.all(limit));
|
|
376
|
+
return (rows ?? []).map((row) => {
|
|
377
|
+
const record = row;
|
|
378
|
+
return {
|
|
379
|
+
startedAt: String(record.started_at),
|
|
380
|
+
finishedAt: optional(record.finished_at),
|
|
381
|
+
durationMs: record.duration_ms === null ? undefined : Number(record.duration_ms),
|
|
382
|
+
target: String(record.target),
|
|
383
|
+
name: optional(record.name),
|
|
384
|
+
slug: optional(record.slug),
|
|
385
|
+
visibility: optional(record.visibility),
|
|
386
|
+
url: optional(record.url),
|
|
387
|
+
status: String(record.status),
|
|
388
|
+
pagesCount: record.pages_count === null ? undefined : Number(record.pages_count),
|
|
389
|
+
pagesCreated: record.pages_created === null ? undefined : Number(record.pages_created),
|
|
390
|
+
pagesUpdated: record.pages_updated === null ? undefined : Number(record.pages_updated),
|
|
391
|
+
pagesDeleted: record.pages_deleted === null ? undefined : Number(record.pages_deleted),
|
|
392
|
+
error: optional(record.error_message),
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Import the JSON state written before history existed, so an upgraded project
|
|
398
|
+
* does not start empty. Runs once; the marker keeps later commands cheap.
|
|
399
|
+
*/
|
|
400
|
+
export async function backfillHistory(root, force = false) {
|
|
401
|
+
const database = await openHistory(root);
|
|
402
|
+
if (!database)
|
|
403
|
+
return 0;
|
|
404
|
+
try {
|
|
405
|
+
if (!force) {
|
|
406
|
+
// Version 2 re-imports retained run manifests once so older create runs
|
|
407
|
+
// that were incorrectly stored as updates receive the right action.
|
|
408
|
+
const marker = database.prepare("SELECT value FROM meta WHERE key = 'backfilled_at_v2'").get();
|
|
409
|
+
if (marker)
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
let imported = 0;
|
|
413
|
+
const runsRoot = join(root, '.doxloop', 'runs');
|
|
414
|
+
if (await pathExists(runsRoot)) {
|
|
415
|
+
const { readdir } = await import('node:fs/promises');
|
|
416
|
+
const entries = await readdir(runsRoot, { withFileTypes: true });
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
if (!entry.isDirectory())
|
|
419
|
+
continue;
|
|
420
|
+
const manifest = join(runsRoot, entry.name, 'run.json');
|
|
421
|
+
if (!(await pathExists(manifest)))
|
|
422
|
+
continue;
|
|
423
|
+
try {
|
|
424
|
+
const raw = JSON.parse(await readFile(manifest, 'utf8'));
|
|
425
|
+
if ((raw.schemaVersion !== 1 && raw.schemaVersion !== 2) || !Array.isArray(raw.changes))
|
|
426
|
+
continue;
|
|
427
|
+
await recordSyncRun(root, raw, { runDir: join('.doxloop', 'runs', entry.name) });
|
|
428
|
+
imported += 1;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// An interrupted run directory is not history worth importing.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const syncState = join(root, '.doxloop', 'sync-state.json');
|
|
436
|
+
if (await pathExists(syncState)) {
|
|
437
|
+
try {
|
|
438
|
+
await recordSourceSyncs(root, JSON.parse(await readFile(syncState, 'utf8')));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// A malformed baseline file simply contributes nothing.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await syncPageRegistry(root);
|
|
445
|
+
database
|
|
446
|
+
.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('backfilled_at_v2', ?)")
|
|
447
|
+
.run(now());
|
|
448
|
+
return imported;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export function formatRequestHistory(requests) {
|
|
455
|
+
if (requests.length === 0) {
|
|
456
|
+
return 'No documentation history recorded yet.';
|
|
457
|
+
}
|
|
458
|
+
const lines = requests.map((request) => {
|
|
459
|
+
const when = request.createdAt.replace('T', ' ').slice(0, 16);
|
|
460
|
+
const changes = request.pagesChanged > 0
|
|
461
|
+
? `${request.pagesChanged} page${request.pagesChanged === 1 ? '' : 's'} (+${request.linesAdded}/-${request.linesRemoved})`
|
|
462
|
+
: 'no changes';
|
|
463
|
+
const detail = request.requestText
|
|
464
|
+
? `\n "${truncate(request.requestText, 96)}"`
|
|
465
|
+
: request.sourceSummary
|
|
466
|
+
? `\n ${truncate(request.sourceSummary, 96)}`
|
|
467
|
+
: '';
|
|
468
|
+
return ` ${statusMark(request.status)} ${when} ${request.kind.padEnd(6)} ${changes}${request.agent ? ` · ${request.agent}` : ''}${detail}`;
|
|
469
|
+
});
|
|
470
|
+
return `Documentation history (${requests.length} most recent)\n${lines.join('\n')}`;
|
|
471
|
+
}
|
|
472
|
+
function statusMark(status) {
|
|
473
|
+
if (status === 'applied')
|
|
474
|
+
return '✓';
|
|
475
|
+
if (status === 'rejected')
|
|
476
|
+
return '✗';
|
|
477
|
+
if (status === 'failed')
|
|
478
|
+
return '!';
|
|
479
|
+
if (status === 'running' || status === 'generating')
|
|
480
|
+
return '·';
|
|
481
|
+
return '○';
|
|
482
|
+
}
|
|
483
|
+
function toRequest(row) {
|
|
484
|
+
const record = row;
|
|
485
|
+
return {
|
|
486
|
+
id: String(record.id),
|
|
487
|
+
createdAt: String(record.created_at),
|
|
488
|
+
finishedAt: optional(record.finished_at),
|
|
489
|
+
durationMs: record.duration_ms === null ? undefined : Number(record.duration_ms),
|
|
490
|
+
kind: String(record.kind),
|
|
491
|
+
trigger: String(record.trigger),
|
|
492
|
+
requestText: optional(record.request_text),
|
|
493
|
+
agent: optional(record.agent),
|
|
494
|
+
model: optional(record.model),
|
|
495
|
+
status: String(record.status),
|
|
496
|
+
pagesChanged: Number(record.pages_changed ?? 0),
|
|
497
|
+
linesAdded: Number(record.lines_added ?? 0),
|
|
498
|
+
linesRemoved: Number(record.lines_removed ?? 0),
|
|
499
|
+
validationErrors: record.validation_errors === null ? undefined : Number(record.validation_errors),
|
|
500
|
+
validationWarnings: record.validation_warnings === null ? undefined : Number(record.validation_warnings),
|
|
501
|
+
sourceSummary: optional(record.source_summary),
|
|
502
|
+
error: optional(record.error_message),
|
|
503
|
+
usage: usageFromRow(record),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/** The eight usage columns in table order, all null when the agent reported no usage. */
|
|
507
|
+
function usageColumns(usage) {
|
|
508
|
+
if (!usage)
|
|
509
|
+
return [null, null, null, null, null, null, null, null];
|
|
510
|
+
return [
|
|
511
|
+
usage.inputTokens,
|
|
512
|
+
usage.outputTokens,
|
|
513
|
+
usage.cacheReadTokens,
|
|
514
|
+
usage.cacheCreationTokens,
|
|
515
|
+
usage.costUsd ?? null,
|
|
516
|
+
usage.turns,
|
|
517
|
+
usage.sessions,
|
|
518
|
+
usage.maxContextTokens,
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
/** Rebuild the usage from a request row; undefined when no usage was ever recorded for it. */
|
|
522
|
+
function usageFromRow(record) {
|
|
523
|
+
const count = (value) => value === null || value === undefined ? undefined : Number(value);
|
|
524
|
+
const input = count(record.input_tokens);
|
|
525
|
+
const output = count(record.output_tokens);
|
|
526
|
+
const cacheRead = count(record.cache_read_tokens);
|
|
527
|
+
const cacheCreation = count(record.cache_creation_tokens);
|
|
528
|
+
if (input === undefined && output === undefined && cacheRead === undefined && cacheCreation === undefined) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const cost = count(record.cost_usd);
|
|
532
|
+
return {
|
|
533
|
+
inputTokens: input ?? 0,
|
|
534
|
+
outputTokens: output ?? 0,
|
|
535
|
+
cacheReadTokens: cacheRead ?? 0,
|
|
536
|
+
cacheCreationTokens: cacheCreation ?? 0,
|
|
537
|
+
totalTokens: (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0),
|
|
538
|
+
...(cost !== undefined ? { costUsd: cost } : {}),
|
|
539
|
+
turns: count(record.agent_turns) ?? 0,
|
|
540
|
+
sessions: count(record.agent_sessions) ?? 1,
|
|
541
|
+
maxContextTokens: count(record.max_context_tokens) ?? 0,
|
|
542
|
+
durationMs: count(record.duration_ms) ?? 0,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function changeDecision(change) {
|
|
546
|
+
const decided = change.hunks.filter((hunk) => hunk.acceptedAt || hunk.rejectedAt);
|
|
547
|
+
if (decided.length === 0)
|
|
548
|
+
return { state: 'pending', at: null };
|
|
549
|
+
const accepted = change.hunks.filter((hunk) => hunk.acceptedAt);
|
|
550
|
+
const at = decided
|
|
551
|
+
.map((hunk) => hunk.acceptedAt ?? hunk.rejectedAt ?? '')
|
|
552
|
+
.sort()
|
|
553
|
+
.at(-1) ?? null;
|
|
554
|
+
if (accepted.length === change.hunks.length)
|
|
555
|
+
return { state: 'accepted', at };
|
|
556
|
+
if (accepted.length === 0 && decided.length === change.hunks.length) {
|
|
557
|
+
return { state: 'rejected', at };
|
|
558
|
+
}
|
|
559
|
+
return { state: 'partial', at };
|
|
560
|
+
}
|
|
561
|
+
function lineCounts(changes) {
|
|
562
|
+
let added = 0;
|
|
563
|
+
let removed = 0;
|
|
564
|
+
for (const change of changes) {
|
|
565
|
+
for (const hunk of change.hunks) {
|
|
566
|
+
added += hunk.newLines.length;
|
|
567
|
+
removed += hunk.oldLines.length;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { added, removed };
|
|
571
|
+
}
|
|
572
|
+
function frontmatterTitle(content) {
|
|
573
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
574
|
+
if (!match)
|
|
575
|
+
return undefined;
|
|
576
|
+
const title = /^title:\s*(.+)$/m.exec(match[1] ?? '');
|
|
577
|
+
return title?.[1]?.trim().replace(/^["']|["']$/g, '') || undefined;
|
|
578
|
+
}
|
|
579
|
+
function elapsed(from, to) {
|
|
580
|
+
const start = Date.parse(from);
|
|
581
|
+
const end = Date.parse(to);
|
|
582
|
+
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
|
|
583
|
+
}
|
|
584
|
+
function truncate(value, length) {
|
|
585
|
+
const single = value.replace(/\s+/g, ' ').trim();
|
|
586
|
+
return single.length > length ? `${single.slice(0, length - 1)}…` : single;
|
|
587
|
+
}
|
|
588
|
+
function portable(path) {
|
|
589
|
+
return path.split('\\').join('/');
|
|
590
|
+
}
|
|
591
|
+
function optional(value) {
|
|
592
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
593
|
+
}
|
|
594
|
+
function text(value) {
|
|
595
|
+
return value === undefined || value === '' ? null : value;
|
|
596
|
+
}
|
|
597
|
+
function now() {
|
|
598
|
+
return new Date().toISOString();
|
|
599
|
+
}
|
|
600
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free HTML to Markdown conversion for crawled documentation pages.
|
|
3
|
+
*
|
|
4
|
+
* The converter is intentionally tolerant: documentation sites ship imperfect
|
|
5
|
+
* HTML, and the output feeds an agent that reads Markdown for context rather
|
|
6
|
+
* than a renderer that needs exact fidelity. Navigation chrome, scripts,
|
|
7
|
+
* styles, and hidden elements are dropped before conversion.
|
|
8
|
+
*/
|
|
9
|
+
export interface HtmlNode {
|
|
10
|
+
type: 'element' | 'text';
|
|
11
|
+
name?: string;
|
|
12
|
+
attributes: Record<string, string>;
|
|
13
|
+
children: HtmlNode[];
|
|
14
|
+
text?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface HtmlDocumentSummary {
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
canonical?: string;
|
|
20
|
+
generator?: string;
|
|
21
|
+
language?: string;
|
|
22
|
+
/** Absolute or relative link targets found in the main content and navigation. */
|
|
23
|
+
links: string[];
|
|
24
|
+
/** Image sources found in the main content. */
|
|
25
|
+
images: string[];
|
|
26
|
+
headings: Array<{
|
|
27
|
+
level: number;
|
|
28
|
+
text: string;
|
|
29
|
+
}>;
|
|
30
|
+
markdown: string;
|
|
31
|
+
/** Approximate word count of the converted main content. */
|
|
32
|
+
words: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function decodeEntities(value: string): string;
|
|
35
|
+
/** Parse tolerant HTML into a lightweight tree. */
|
|
36
|
+
export declare function parseHtml(html: string): HtmlNode;
|
|
37
|
+
/** Convert an HTML document into a Markdown summary of its main content. */
|
|
38
|
+
export declare function summarizeHtmlDocument(html: string): HtmlDocumentSummary;
|
|
39
|
+
/**
|
|
40
|
+
* The same summary for a page served as Markdown. Mintlify, GitBook, and
|
|
41
|
+
* other client-rendered docs sites send an empty HTML shell to a crawler but
|
|
42
|
+
* serve each page's source at `<page>.md` (and list those URLs in llms.txt).
|
|
43
|
+
* Their own "documentation index" preamble is dropped.
|
|
44
|
+
*/
|
|
45
|
+
export declare function summarizeMarkdownDocument(source: string): HtmlDocumentSummary;
|
|
46
|
+
//# sourceMappingURL=html-markdown.d.ts.map
|