@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/sync.d.ts
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
import type { SourceBinding, SourceChange, SyncState } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Baseline marker for a local folder that is not a Git repository. Such a
|
|
4
|
+
* source is compared by content: the recorded per-file digests name exactly
|
|
5
|
+
* which files changed, so drift can still be traced to individual pages.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LOCAL_CONTENT_BASELINE = "local-content";
|
|
8
|
+
/**
|
|
9
|
+
* Baseline marker for a crawled documentation-site snapshot. The snapshot is
|
|
10
|
+
* frozen, so a change means the site was re-crawled and the recorded per-page
|
|
11
|
+
* digests name exactly which existing pages changed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DOCS_SITE_BASELINE = "docs-site";
|
|
2
14
|
export declare const SYNC_STATE_FILE: string;
|
|
3
15
|
export declare function readSyncState(root: string): Promise<SyncState>;
|
|
4
16
|
export declare function recordSyncState(root: string, sources: SourceBinding[]): Promise<SyncState>;
|
|
5
17
|
export declare function collectSourceChanges(root: string, sources: SourceBinding[]): Promise<SourceChange[]>;
|
|
18
|
+
/** Name-status lines, in Git's format, describing how one manifest became another. */
|
|
19
|
+
export declare function manifestChanges(before: Record<string, string>, after: Record<string, string>): string[];
|
|
20
|
+
/** Stable content fingerprints used to invalidate an approved planning checkpoint. */
|
|
21
|
+
export declare function sourceSnapshotFingerprints(root: string, sources: SourceBinding[]): Promise<Record<string, string | null>>;
|
|
22
|
+
/**
|
|
23
|
+
* Source-relative paths touched by a change, parsed from the same Git output
|
|
24
|
+
* that produced the human-readable lists. Rename and copy entries contribute
|
|
25
|
+
* both the old and the new path so either one can match documented evidence.
|
|
26
|
+
*/
|
|
27
|
+
export declare function changedSourcePaths(change: SourceChange): string[];
|
|
6
28
|
export declare function formatSourceChanges(changes: SourceChange[]): string;
|
|
29
|
+
export interface LocalSourceManifest {
|
|
30
|
+
/** Source-relative portable paths to their content digests. */
|
|
31
|
+
files: Record<string, string>;
|
|
32
|
+
/** One digest over every path and content, in the historical format. */
|
|
33
|
+
fingerprint: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every file that counts as source content, with its digest. Git repositories
|
|
37
|
+
* contribute tracked and untracked-but-not-ignored files; a plain folder is
|
|
38
|
+
* walked directly with the same exclusions a repository would apply by
|
|
39
|
+
* convention. Secrets are never read.
|
|
40
|
+
*/
|
|
41
|
+
export declare function localSourceManifest(path: string): Promise<LocalSourceManifest>;
|
|
7
42
|
//# sourceMappingURL=sync.d.ts.map
|
package/dist/sync.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { lstat, readFile, readlink, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
3
|
+
import { lstat, readFile, readdir, readlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { pathExists, readJson } from './fs.js';
|
|
7
|
+
import { diffOpenApi, loadOpenApiSource, openApiChangedIdentifiers } from './openapi.js';
|
|
7
8
|
import { isSpecUrl, sourceKind } from './project.js';
|
|
9
|
+
import { remoteHead } from './remote-source.js';
|
|
8
10
|
const SPEC_BASELINE = 'openapi-spec';
|
|
11
|
+
/**
|
|
12
|
+
* Baseline marker for a local folder that is not a Git repository. Such a
|
|
13
|
+
* source is compared by content: the recorded per-file digests name exactly
|
|
14
|
+
* which files changed, so drift can still be traced to individual pages.
|
|
15
|
+
*/
|
|
16
|
+
export const LOCAL_CONTENT_BASELINE = 'local-content';
|
|
17
|
+
/**
|
|
18
|
+
* Baseline marker for a crawled documentation-site snapshot. The snapshot is
|
|
19
|
+
* frozen, so a change means the site was re-crawled and the recorded per-page
|
|
20
|
+
* digests name exactly which existing pages changed.
|
|
21
|
+
*/
|
|
22
|
+
export const DOCS_SITE_BASELINE = 'docs-site';
|
|
9
23
|
export const SYNC_STATE_FILE = join('.doxloop', 'sync-state.json');
|
|
10
24
|
const MAX_LISTED_FILES = 40;
|
|
11
25
|
const run = promisify(execFile);
|
|
@@ -23,26 +37,58 @@ export async function recordSyncState(root, sources) {
|
|
|
23
37
|
const state = { schemaVersion: 1, sources: {} };
|
|
24
38
|
for (const source of sources) {
|
|
25
39
|
if (sourceKind(source) === 'openapi') {
|
|
26
|
-
|
|
40
|
+
const loaded = await loadOpenApiSource(root, source);
|
|
41
|
+
state.sources[source.name] = {
|
|
42
|
+
commit: SPEC_BASELINE,
|
|
43
|
+
recordedAt: new Date().toISOString(),
|
|
44
|
+
contentFingerprint: loaded.hash,
|
|
45
|
+
connector: {
|
|
46
|
+
id: 'openapi',
|
|
47
|
+
version: 1,
|
|
48
|
+
...(loaded.etag ? { etag: loaded.etag } : {}),
|
|
49
|
+
...(loaded.lastModified ? { lastModified: loaded.lastModified } : {}),
|
|
50
|
+
openapi: loaded.snapshot,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (source.remote) {
|
|
56
|
+
state.sources[source.name] = {
|
|
57
|
+
commit: await remoteHead(source.remote),
|
|
58
|
+
recordedAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const sourcePath = resolve(root, source.path);
|
|
63
|
+
if (sourceKind(source) === 'docs-site') {
|
|
64
|
+
if (!(await pathExists(sourcePath)))
|
|
27
65
|
continue;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
66
|
+
const manifest = await localSourceManifest(sourcePath);
|
|
67
|
+
state.sources[source.name] = {
|
|
68
|
+
commit: DOCS_SITE_BASELINE,
|
|
69
|
+
recordedAt: new Date().toISOString(),
|
|
70
|
+
contentFingerprint: manifest.fingerprint,
|
|
71
|
+
files: manifest.files,
|
|
72
|
+
};
|
|
36
73
|
continue;
|
|
37
74
|
}
|
|
38
|
-
const commit = await headCommit(
|
|
75
|
+
const commit = await headCommit(sourcePath);
|
|
39
76
|
if (commit) {
|
|
40
|
-
const sourcePath = resolve(root, source.path);
|
|
41
77
|
state.sources[source.name] = {
|
|
42
78
|
commit,
|
|
43
79
|
recordedAt: new Date().toISOString(),
|
|
44
80
|
contentFingerprint: await sourceContentFingerprint(sourcePath),
|
|
45
81
|
};
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (await pathExists(sourcePath)) {
|
|
85
|
+
const manifest = await localSourceManifest(sourcePath);
|
|
86
|
+
state.sources[source.name] = {
|
|
87
|
+
commit: LOCAL_CONTENT_BASELINE,
|
|
88
|
+
recordedAt: new Date().toISOString(),
|
|
89
|
+
contentFingerprint: manifest.fingerprint,
|
|
90
|
+
files: manifest.files,
|
|
91
|
+
};
|
|
46
92
|
}
|
|
47
93
|
}
|
|
48
94
|
await writeFile(join(root, SYNC_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
@@ -61,9 +107,23 @@ export async function collectSourceChanges(root, sources) {
|
|
|
61
107
|
changes.push({ ...source, kind: 'missing-path' });
|
|
62
108
|
continue;
|
|
63
109
|
}
|
|
110
|
+
if (sourceKind(source) === 'docs-site') {
|
|
111
|
+
changes.push(await collectLocalContentChange(source, path, state, DOCS_SITE_BASELINE));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
64
114
|
const head = await headCommit(path);
|
|
65
115
|
if (!head) {
|
|
66
|
-
|
|
116
|
+
// A remote source materialized from a provider is an immutable snapshot
|
|
117
|
+
// pinned at the recorded commit: the folder is named after that commit
|
|
118
|
+
// and nothing inside it can drift, so it is current by construction.
|
|
119
|
+
// Treating it as a baseline-free plain folder made every page of a
|
|
120
|
+
// GitHub-sourced project count as unverified.
|
|
121
|
+
const record = state.sources[source.name];
|
|
122
|
+
if (source.remote && record && record.commit !== LOCAL_CONTENT_BASELINE && basename(path) === record.commit) {
|
|
123
|
+
changes.push({ ...source, kind: 'unchanged', baseline: record.commit, head: record.commit, changedFiles: [], uncommittedFiles: [] });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
changes.push(await collectLocalContentChange(source, path, state));
|
|
67
127
|
continue;
|
|
68
128
|
}
|
|
69
129
|
const uncommittedFiles = await uncommittedChanges(path);
|
|
@@ -95,32 +155,150 @@ export async function collectSourceChanges(root, sources) {
|
|
|
95
155
|
}
|
|
96
156
|
return changes;
|
|
97
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A folder without Git history is compared against the file digests recorded
|
|
160
|
+
* at the last sync, so a scheduled check on a plain folder still names the
|
|
161
|
+
* files that changed.
|
|
162
|
+
*/
|
|
163
|
+
async function collectLocalContentChange(source, path, state, baseline = LOCAL_CONTENT_BASELINE) {
|
|
164
|
+
const record = state.sources[source.name];
|
|
165
|
+
if (!record || record.commit !== baseline || !record.files) {
|
|
166
|
+
return { ...source, kind: 'no-baseline', head: baseline, uncommittedFiles: [] };
|
|
167
|
+
}
|
|
168
|
+
const manifest = await localSourceManifest(path);
|
|
169
|
+
const changedFiles = record.contentFingerprint === manifest.fingerprint
|
|
170
|
+
? []
|
|
171
|
+
: manifestChanges(record.files, manifest.files);
|
|
172
|
+
return {
|
|
173
|
+
...source,
|
|
174
|
+
kind: changedFiles.length === 0 ? 'unchanged' : 'changed',
|
|
175
|
+
baseline,
|
|
176
|
+
head: baseline,
|
|
177
|
+
changedFiles,
|
|
178
|
+
uncommittedFiles: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Name-status lines, in Git's format, describing how one manifest became another. */
|
|
182
|
+
export function manifestChanges(before, after) {
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const file of Object.keys(after).sort()) {
|
|
185
|
+
if (!(file in before))
|
|
186
|
+
lines.push(`A\t${file}`);
|
|
187
|
+
else if (before[file] !== after[file])
|
|
188
|
+
lines.push(`M\t${file}`);
|
|
189
|
+
}
|
|
190
|
+
for (const file of Object.keys(before).sort()) {
|
|
191
|
+
if (!(file in after))
|
|
192
|
+
lines.push(`D\t${file}`);
|
|
193
|
+
}
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
196
|
+
/** Stable content fingerprints used to invalidate an approved planning checkpoint. */
|
|
197
|
+
export async function sourceSnapshotFingerprints(root, sources) {
|
|
198
|
+
const fingerprints = {};
|
|
199
|
+
for (const source of sources) {
|
|
200
|
+
if (sourceKind(source) === 'openapi') {
|
|
201
|
+
try {
|
|
202
|
+
fingerprints[source.name] = (await loadOpenApiSource(root, source)).hash;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
fingerprints[source.name] = null;
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
fingerprints[source.name] = await sourceContentFingerprint(resolve(root, source.path));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
fingerprints[source.name] = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return fingerprints;
|
|
217
|
+
}
|
|
98
218
|
async function collectSpecChange(root, source, state) {
|
|
99
|
-
if (isSpecUrl(source.path))
|
|
100
|
-
return { ...source, kind: 'spec-remote' };
|
|
101
|
-
const fingerprint = await specFileFingerprint(resolve(root, source.path));
|
|
102
|
-
if (!fingerprint)
|
|
103
|
-
return { ...source, kind: 'missing-path' };
|
|
104
219
|
const record = state.sources[source.name];
|
|
105
|
-
|
|
106
|
-
|
|
220
|
+
let loaded;
|
|
221
|
+
try {
|
|
222
|
+
loaded = await loadOpenApiSource(root, source, record?.connector ? {
|
|
223
|
+
...(record.connector.etag ? { etag: record.connector.etag } : {}),
|
|
224
|
+
...(record.connector.lastModified ? { lastModified: record.connector.lastModified } : {}),
|
|
225
|
+
} : undefined);
|
|
107
226
|
}
|
|
108
|
-
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (!isSpecUrl(source.path) && !(await pathExists(resolve(root, source.path)))) {
|
|
229
|
+
return { ...source, kind: 'missing-path' };
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
if (record?.contentFingerprint === loaded.hash || loaded.notModified) {
|
|
234
|
+
return { ...source, kind: 'spec-unchanged', head: loaded.hash, summary: loaded.summary };
|
|
235
|
+
}
|
|
236
|
+
const apiDiff = diffOpenApi(record?.connector?.openapi, loaded.snapshot);
|
|
237
|
+
return {
|
|
238
|
+
...source,
|
|
239
|
+
kind: 'spec-changed',
|
|
240
|
+
...(record?.contentFingerprint ? { baseline: record.contentFingerprint } : {}),
|
|
241
|
+
head: loaded.hash,
|
|
242
|
+
summary: loaded.summary,
|
|
243
|
+
apiDiff,
|
|
244
|
+
changedIdentifiers: openApiChangedIdentifiers(apiDiff),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Source-relative paths touched by a change, parsed from the same Git output
|
|
249
|
+
* that produced the human-readable lists. Rename and copy entries contribute
|
|
250
|
+
* both the old and the new path so either one can match documented evidence.
|
|
251
|
+
*/
|
|
252
|
+
export function changedSourcePaths(change) {
|
|
253
|
+
if (change.kind === 'spec-changed')
|
|
254
|
+
return change.changedIdentifiers;
|
|
255
|
+
if (change.kind !== 'changed' && change.kind !== 'no-baseline' && change.kind !== 'baseline-lost') {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const paths = new Set();
|
|
259
|
+
if (change.kind === 'changed') {
|
|
260
|
+
for (const line of change.changedFiles) {
|
|
261
|
+
for (const path of parseNameStatus(line))
|
|
262
|
+
paths.add(path);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const line of change.uncommittedFiles) {
|
|
266
|
+
for (const path of parsePorcelain(line))
|
|
267
|
+
paths.add(path);
|
|
268
|
+
}
|
|
269
|
+
return [...paths];
|
|
270
|
+
}
|
|
271
|
+
/** `M<tab>src/auth.ts` or `R100<tab>old.ts<tab>new.ts` */
|
|
272
|
+
function parseNameStatus(line) {
|
|
273
|
+
const fields = line.split('\t');
|
|
274
|
+
return fields.slice(1).map(unquotePath).filter(Boolean);
|
|
109
275
|
}
|
|
110
|
-
|
|
276
|
+
/** `M src/auth.ts`, `?? new.ts`, or `R old.ts -> new.ts`, already trimmed. */
|
|
277
|
+
function parsePorcelain(line) {
|
|
278
|
+
const match = /^\S+\s+(.*)$/.exec(line);
|
|
279
|
+
if (!match?.[1])
|
|
280
|
+
return [];
|
|
281
|
+
return match[1]
|
|
282
|
+
.split(' -> ')
|
|
283
|
+
.map((part) => unquotePath(part))
|
|
284
|
+
.filter(Boolean);
|
|
285
|
+
}
|
|
286
|
+
function unquotePath(value) {
|
|
287
|
+
const path = value.trim();
|
|
288
|
+
if (!path.startsWith('"') || !path.endsWith('"'))
|
|
289
|
+
return path;
|
|
111
290
|
try {
|
|
112
|
-
|
|
113
|
-
return createHash('sha256').update(content).digest('hex');
|
|
291
|
+
return String(JSON.parse(path));
|
|
114
292
|
}
|
|
115
293
|
catch {
|
|
116
|
-
return
|
|
294
|
+
return path.slice(1, -1);
|
|
117
295
|
}
|
|
118
296
|
}
|
|
119
297
|
export function formatSourceChanges(changes) {
|
|
120
298
|
if (changes.length === 0)
|
|
121
299
|
return '';
|
|
122
300
|
const sections = changes.map((change) => {
|
|
123
|
-
const heading = `Source "${change.name}" (${change.path})`;
|
|
301
|
+
const heading = `Source "${change.name}" (${change.site?.url ?? change.path})`;
|
|
124
302
|
switch (change.kind) {
|
|
125
303
|
case 'missing-path':
|
|
126
304
|
return `${heading}: the configured path does not exist. Report this instead of guessing.`;
|
|
@@ -129,16 +307,35 @@ export function formatSourceChanges(changes) {
|
|
|
129
307
|
case 'spec-unchanged':
|
|
130
308
|
return `${heading}: the API specification is unchanged since the last documentation sync.`;
|
|
131
309
|
case 'spec-changed':
|
|
132
|
-
return `${heading}: the API specification changed
|
|
310
|
+
return `${heading}: the API specification changed${change.baseline ? ` (${short(change.baseline)} -> ${short(change.head)})` : ' and has no recorded baseline'}.
|
|
311
|
+
${formatApiDelta(change.apiDiff)}
|
|
312
|
+
Update only documentation affected by this structural delta; the new baseline is recorded after acceptance.`;
|
|
133
313
|
case 'not-git':
|
|
134
314
|
return `${heading}: not a Git repository, so no change baseline is available. Inspect the source directly.`;
|
|
135
315
|
case 'no-baseline':
|
|
316
|
+
if (change.head === DOCS_SITE_BASELINE) {
|
|
317
|
+
return `${heading}: existing documentation site, crawled into the read-only snapshot at ${change.path}. No documentation sync baseline is recorded yet; read the snapshot's index.md and page files directly. A baseline is recorded when this task completes.`;
|
|
318
|
+
}
|
|
136
319
|
return withUncommitted(`${heading}: no documentation sync baseline is recorded yet. Inspect the source directly; a baseline is recorded when this task completes.`, change.uncommittedFiles);
|
|
137
320
|
case 'baseline-lost':
|
|
138
321
|
return withUncommitted(`${heading}: the recorded baseline commit no longer exists (history rewritten?). Inspect the source directly; the baseline is re-recorded when this task completes.`, change.uncommittedFiles);
|
|
139
322
|
case 'unchanged':
|
|
140
|
-
|
|
323
|
+
if (change.baseline === DOCS_SITE_BASELINE) {
|
|
324
|
+
return `${heading}: the existing documentation snapshot at ${change.path} is unchanged since the last documentation sync.`;
|
|
325
|
+
}
|
|
326
|
+
return withUncommitted(`${heading}: no source-content changes since the last documentation sync (${change.baseline === LOCAL_CONTENT_BASELINE ? 'compared by content' : short(change.baseline)}).`, change.uncommittedFiles);
|
|
141
327
|
case 'changed':
|
|
328
|
+
if (change.baseline === DOCS_SITE_BASELINE) {
|
|
329
|
+
return `${heading}: the existing documentation site was re-crawled and these snapshot pages changed:\n${fileList(change.changedFiles)}\nRead the changed pages in the snapshot at ${change.path} and decide whether the rewritten documentation must follow.`;
|
|
330
|
+
}
|
|
331
|
+
if (change.remote) {
|
|
332
|
+
return change.changedFiles.length > 0
|
|
333
|
+
? `${heading}: the remote repository changed (${short(change.baseline)} -> ${short(change.head)}).\nChanged files reported by ${change.remote.provider}:\n${fileList(change.changedFiles)}\nThe current commit was downloaded into this isolated read-only evidence snapshot; inspect files there directly.`
|
|
334
|
+
: `${heading}: the remote repository changed (${short(change.baseline)} -> ${short(change.head)}). Inspect the isolated evidence snapshot directly.`;
|
|
335
|
+
}
|
|
336
|
+
if (change.baseline === LOCAL_CONTENT_BASELINE) {
|
|
337
|
+
return `${heading}: files changed since the last documentation sync (compared by content; this folder has no Git history).\nChanged files:\n${fileList(change.changedFiles)}\nInspect the current files directly.`;
|
|
338
|
+
}
|
|
142
339
|
return withUncommitted(change.changedFiles.length > 0
|
|
143
340
|
? `${heading}: changed since the last documentation sync (${short(change.baseline)} -> ${short(change.head)}).\nCommitted changes:\n${fileList(change.changedFiles)}\nInspect details with \`git -C ${change.path} diff ${short(change.baseline)}..HEAD -- <file>\`.`
|
|
144
341
|
: `${heading}: the working tree changed since the last documentation sync (${short(change.baseline)}).`, change.uncommittedFiles);
|
|
@@ -146,6 +343,28 @@ export function formatSourceChanges(changes) {
|
|
|
146
343
|
});
|
|
147
344
|
return `Source changes since the last documentation sync:\n\n${sections.join('\n\n')}`;
|
|
148
345
|
}
|
|
346
|
+
function formatApiDelta(diff) {
|
|
347
|
+
const lines = ['Structural API delta:'];
|
|
348
|
+
for (const item of diff.operations.added)
|
|
349
|
+
lines.push(`- operation added: ${item}`);
|
|
350
|
+
for (const item of diff.operations.removed)
|
|
351
|
+
lines.push(`- operation removed: ${item}`);
|
|
352
|
+
for (const item of diff.operations.changed)
|
|
353
|
+
lines.push(`- operation changed: ${item.id} (${item.facets.join(', ')})`);
|
|
354
|
+
for (const item of diff.schemas.added)
|
|
355
|
+
lines.push(`- schema added: ${item}`);
|
|
356
|
+
for (const item of diff.schemas.removed)
|
|
357
|
+
lines.push(`- schema removed: ${item}`);
|
|
358
|
+
for (const item of diff.schemas.changed)
|
|
359
|
+
lines.push(`- schema changed: ${item.id}`);
|
|
360
|
+
for (const item of diff.securitySchemes.added)
|
|
361
|
+
lines.push(`- authentication added: ${item}`);
|
|
362
|
+
for (const item of diff.securitySchemes.removed)
|
|
363
|
+
lines.push(`- authentication removed: ${item}`);
|
|
364
|
+
for (const item of diff.securitySchemes.changed)
|
|
365
|
+
lines.push(`- authentication changed: ${item.id}`);
|
|
366
|
+
return lines.length === 1 ? `${lines[0]} no public structural changes detected.` : lines.join('\n');
|
|
367
|
+
}
|
|
149
368
|
function withUncommitted(text, uncommittedFiles) {
|
|
150
369
|
if (uncommittedFiles.length === 0)
|
|
151
370
|
return text;
|
|
@@ -183,14 +402,26 @@ async function uncommittedChanges(path) {
|
|
|
183
402
|
return lines(output).map((line) => line.trim());
|
|
184
403
|
}
|
|
185
404
|
async function sourceContentFingerprint(path) {
|
|
405
|
+
return (await localSourceManifest(path)).fingerprint;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Every file that counts as source content, with its digest. Git repositories
|
|
409
|
+
* contribute tracked and untracked-but-not-ignored files; a plain folder is
|
|
410
|
+
* walked directly with the same exclusions a repository would apply by
|
|
411
|
+
* convention. Secrets are never read.
|
|
412
|
+
*/
|
|
413
|
+
export async function localSourceManifest(path) {
|
|
186
414
|
const output = await git(path, [
|
|
187
415
|
'ls-files',
|
|
188
416
|
'-co',
|
|
189
417
|
'--exclude-standard',
|
|
190
418
|
'-z',
|
|
191
419
|
]);
|
|
192
|
-
const files =
|
|
420
|
+
const files = output === undefined
|
|
421
|
+
? await walkPlainFolder(path)
|
|
422
|
+
: output.split('\0').filter(Boolean).sort();
|
|
193
423
|
const hash = createHash('sha256');
|
|
424
|
+
const manifest = {};
|
|
194
425
|
for (const file of files) {
|
|
195
426
|
if (isSensitiveSourcePath(file))
|
|
196
427
|
continue;
|
|
@@ -215,8 +446,34 @@ async function sourceContentFingerprint(path) {
|
|
|
215
446
|
if (content !== undefined)
|
|
216
447
|
hash.update(content);
|
|
217
448
|
hash.update('\0');
|
|
449
|
+
manifest[file] = createHash('sha256').update(content ?? '').digest('hex');
|
|
218
450
|
}
|
|
219
|
-
return hash.digest('hex');
|
|
451
|
+
return { files: manifest, fingerprint: hash.digest('hex') };
|
|
452
|
+
}
|
|
453
|
+
const PLAIN_FOLDER_IGNORED = new Set(['.git', 'node_modules', '.doxloop-sources', '.doxloop']);
|
|
454
|
+
async function walkPlainFolder(root) {
|
|
455
|
+
const files = [];
|
|
456
|
+
const visit = async (directory) => {
|
|
457
|
+
let entries;
|
|
458
|
+
try {
|
|
459
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
if (PLAIN_FOLDER_IGNORED.has(entry.name))
|
|
466
|
+
continue;
|
|
467
|
+
const absolute = join(directory, entry.name);
|
|
468
|
+
if (entry.isDirectory())
|
|
469
|
+
await visit(absolute);
|
|
470
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
471
|
+
files.push(relative(root, absolute).split('\\').join('/'));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
await visit(root);
|
|
476
|
+
return files.sort();
|
|
220
477
|
}
|
|
221
478
|
function isSensitiveSourcePath(path) {
|
|
222
479
|
const segments = path.toLowerCase().split(/[\\/]/);
|
|
@@ -262,6 +519,15 @@ function isSourceRecords(value) {
|
|
|
262
519
|
(record.contentFingerprint ===
|
|
263
520
|
undefined ||
|
|
264
521
|
typeof record
|
|
265
|
-
.contentFingerprint === 'string')
|
|
522
|
+
.contentFingerprint === 'string') &&
|
|
523
|
+
isFileManifest(record.files)));
|
|
524
|
+
}
|
|
525
|
+
function isFileManifest(value) {
|
|
526
|
+
if (value === undefined)
|
|
527
|
+
return true;
|
|
528
|
+
return (value !== null &&
|
|
529
|
+
typeof value === 'object' &&
|
|
530
|
+
!Array.isArray(value) &&
|
|
531
|
+
Object.values(value).every((digest) => typeof digest === 'string'));
|
|
266
532
|
}
|
|
267
533
|
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SyncChangeHunk } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Line diffing shared by the proposal pipeline and the history index. It lives
|
|
4
|
+
* on its own so history can measure an agent's edits without importing the
|
|
5
|
+
* synchronization module that already records into history.
|
|
6
|
+
*/
|
|
7
|
+
export declare function textLines(text: string): string[];
|
|
8
|
+
export declare function lineHunks(before: string, after: string, prefix: string): SyncChangeHunk[];
|
|
9
|
+
//# sourceMappingURL=text-diff.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line diffing shared by the proposal pipeline and the history index. It lives
|
|
3
|
+
* on its own so history can measure an agent's edits without importing the
|
|
4
|
+
* synchronization module that already records into history.
|
|
5
|
+
*/
|
|
6
|
+
export function textLines(text) {
|
|
7
|
+
if (text === '')
|
|
8
|
+
return [];
|
|
9
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
10
|
+
return normalized.endsWith('\n') ? normalized.slice(0, -1).split('\n') : normalized.split('\n');
|
|
11
|
+
}
|
|
12
|
+
export function lineHunks(before, after, prefix) {
|
|
13
|
+
if (before === after)
|
|
14
|
+
return [];
|
|
15
|
+
const oldLines = textLines(before);
|
|
16
|
+
const newLines = textLines(after);
|
|
17
|
+
if (oldLines.length * newLines.length > 2_000_000) {
|
|
18
|
+
return [{ id: `${prefix}-1`, oldStart: 0, oldLines, newStart: 0, newLines }];
|
|
19
|
+
}
|
|
20
|
+
const table = Array.from({ length: oldLines.length + 1 }, () => new Uint32Array(newLines.length + 1));
|
|
21
|
+
for (let old = oldLines.length - 1; old >= 0; old -= 1) {
|
|
22
|
+
for (let next = newLines.length - 1; next >= 0; next -= 1) {
|
|
23
|
+
table[old][next] = oldLines[old] === newLines[next]
|
|
24
|
+
? table[old + 1][next + 1] + 1
|
|
25
|
+
: Math.max(table[old + 1][next], table[old][next + 1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const hunks = [];
|
|
29
|
+
let old = 0;
|
|
30
|
+
let next = 0;
|
|
31
|
+
let current;
|
|
32
|
+
const flush = () => {
|
|
33
|
+
if (!current)
|
|
34
|
+
return;
|
|
35
|
+
current.id = `${prefix}-${hunks.length + 1}`;
|
|
36
|
+
hunks.push(current);
|
|
37
|
+
current = undefined;
|
|
38
|
+
};
|
|
39
|
+
while (old < oldLines.length || next < newLines.length) {
|
|
40
|
+
if (old < oldLines.length && next < newLines.length && oldLines[old] === newLines[next]) {
|
|
41
|
+
flush();
|
|
42
|
+
old += 1;
|
|
43
|
+
next += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
current ??= { id: '', oldStart: old, oldLines: [], newStart: next, newLines: [] };
|
|
47
|
+
if (next < newLines.length && (old >= oldLines.length || table[old][next + 1] >= table[old + 1][next])) {
|
|
48
|
+
current.newLines.push(newLines[next]);
|
|
49
|
+
next += 1;
|
|
50
|
+
}
|
|
51
|
+
else if (old < oldLines.length) {
|
|
52
|
+
current.oldLines.push(oldLines[old]);
|
|
53
|
+
old += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
flush();
|
|
57
|
+
return hunks;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=text-diff.js.map
|