@fro.bot/wiki-write-core 0.1.2 → 0.1.4
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/dist/code-regions.js +34 -15
- package/dist/corrections-survival.d.ts +7 -1
- package/dist/corrections-survival.js +48 -41
- package/dist/gate-contract.js +1 -1
- package/dist/gate-contract.json +1 -1
- package/dist/inline-links.d.ts +35 -0
- package/dist/inline-links.js +120 -0
- package/dist/line-scanners.d.ts +31 -0
- package/dist/line-scanners.js +111 -0
- package/dist/link-labels.d.ts +22 -0
- package/dist/link-labels.js +71 -0
- package/dist/markdown-links.js +11 -9
- package/dist/rendering-policy.js +18 -4
- package/dist/repo-page-move.js +3 -3
- package/dist/wiki-ingest.d.ts +41 -3
- package/dist/wiki-ingest.js +272 -36
- package/dist/wiki-utils.js +7 -4
- package/package.json +1 -1
package/dist/markdown-links.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, normalize } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { findCodeRegions, findRegionAt } from "./code-regions.js";
|
|
5
|
-
|
|
5
|
+
import { createInlineLinkFinder } from "./inline-links.js";
|
|
6
6
|
/** Resolve live relative markdown links and report whether their targets exist. */
|
|
7
7
|
export function resolveMarkdownLinks(content, containingFile, options = {}) {
|
|
8
8
|
const links = [];
|
|
@@ -11,12 +11,14 @@ export function resolveMarkdownLinks(content, containingFile, options = {}) {
|
|
|
11
11
|
for (const [lineIndex, line] of content.split('\n').entries()) {
|
|
12
12
|
const currentLineOffset = lineOffset;
|
|
13
13
|
lineOffset += line.length + 1;
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// Each line gets its own finder; the loop asks it for the next link at or after `from`.
|
|
15
|
+
const findLink = createInlineLinkFinder(line);
|
|
16
|
+
let from = 0;
|
|
17
|
+
for (let match = findLink(from); match !== undefined; match = findLink(from)) {
|
|
18
|
+
const { target } = match;
|
|
18
19
|
const matchIndex = match.index;
|
|
19
|
-
|
|
20
|
+
from = matchIndex + match.length;
|
|
21
|
+
if (matchIndex > 0 && line[matchIndex - 1] === '!') {
|
|
20
22
|
continue;
|
|
21
23
|
}
|
|
22
24
|
// Code binds tighter than links, so a match that touches a fence or span is literal text.
|
|
@@ -24,14 +26,14 @@ export function resolveMarkdownLinks(content, containingFile, options = {}) {
|
|
|
24
26
|
// after an unclosed bracket in code. Resume after the region the match opened in, or just
|
|
25
27
|
// past its first character when it only closes inside one.
|
|
26
28
|
const matchStart = currentLineOffset + matchIndex;
|
|
27
|
-
const matchLast = matchStart + match
|
|
29
|
+
const matchLast = matchStart + match.length - 1;
|
|
28
30
|
const openingRegion = findRegionAt(regions, matchStart);
|
|
29
31
|
if (openingRegion !== undefined) {
|
|
30
|
-
|
|
32
|
+
from = Math.max(openingRegion.end - currentLineOffset, matchIndex + 1);
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
35
|
if (findRegionAt(regions, matchLast) !== undefined) {
|
|
34
|
-
|
|
36
|
+
from = matchIndex + 1;
|
|
35
37
|
continue;
|
|
36
38
|
}
|
|
37
39
|
const normalizedTarget = target.startsWith('<') && target.endsWith('>') ? target.slice(1, -1) : target;
|
package/dist/rendering-policy.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// A tag is unsafe when its name opens it and a `>` follows anywhere after the name. The pattern stops at the
|
|
2
|
+
// name and `hasUnsafeTag` looks for the `>`: a `[^>]*>` tail rescans the rest of the text from every opening
|
|
3
|
+
// when there is no `>`, which is quadratic in the length of the text.
|
|
4
|
+
const UNSAFE_TAG_OPENING_PATTERN = /<\/?\s*(?:script|iframe|object|embed|applet|base|form|input|button|textarea|select|option|meta|link|style|template|svg|math)\b/iu;
|
|
5
|
+
// Both attribute patterns start at the first whitespace character of a run: a match that starts inside a run also
|
|
6
|
+
// starts at the run's beginning, and without the lookbehind a long run of whitespace is rescanned from every
|
|
7
|
+
// position in it.
|
|
8
|
+
const EVENT_HANDLER_PATTERN = /(?<!\s)\s+on[a-z][\w:-]*\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/iu;
|
|
9
|
+
const UNSAFE_URI_PATTERN = /(?<!\s)\s+(?:href|src|srcset|action|formaction|xlink:href|poster|background|cite)\s*=\s*(?:"\s*|')?(?:javascript|vbscript|data):/iu;
|
|
10
|
+
/**
|
|
11
|
+
* True when `text` holds an unsafe tag: an opening `<name` or `</name` from the list, then a `>` after the name.
|
|
12
|
+
* The first opening ends before any later one, so if it has no `>` after it none does.
|
|
13
|
+
*/
|
|
14
|
+
function hasUnsafeTag(text) {
|
|
15
|
+
const opening = UNSAFE_TAG_OPENING_PATTERN.exec(text);
|
|
16
|
+
return opening !== null && text.includes('>', opening.index + opening[0].length);
|
|
17
|
+
}
|
|
4
18
|
/** Return save-side findings for HTML that the render policy will strip. */
|
|
5
19
|
export function validateRenderingPolicy(params) {
|
|
6
20
|
const findings = [];
|
|
7
21
|
const renderableMarkdown = maskCodeContent(params.content);
|
|
8
|
-
if (
|
|
22
|
+
if (hasUnsafeTag(renderableMarkdown) ||
|
|
9
23
|
EVENT_HANDLER_PATTERN.test(renderableMarkdown) ||
|
|
10
24
|
UNSAFE_URI_PATTERN.test(renderableMarkdown)) {
|
|
11
25
|
findings.push({
|
package/dist/repo-page-move.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - `log.md` history and `knowledge/wiki/README.md` are never rewritten.
|
|
15
15
|
*/
|
|
16
16
|
import { parseFrontmatterDocument, renderFrontmatterDocument } from "./frontmatter.js";
|
|
17
|
+
import { parseRelatedFlow } from "./line-scanners.js";
|
|
17
18
|
import { appendLogEntry, rebuildWikiIndex, validateWikilinks, WikiIngestError } from "./wiki-ingest.js";
|
|
18
19
|
import { buildPrivateNameTokens, computeRepoSlug } from "./wiki-slug.js";
|
|
19
20
|
import { findWikilinkSpans } from "./wiki-utils.js";
|
|
@@ -22,7 +23,6 @@ const LOG_PATH = 'knowledge/log.md';
|
|
|
22
23
|
const REPOS_DIR = 'knowledge/wiki/repos';
|
|
23
24
|
const WIKI_PAGE_PATTERN = /^knowledge\/wiki\/(?:repos|topics|entities|comparisons)\/[^/]+\.md$/u;
|
|
24
25
|
const RELATED_BLOCK_KEY = /^related:\s*(?:#.*)?$/u;
|
|
25
|
-
const RELATED_FLOW = /^related:\s*\[(.*)\]\s*(?:#.*)?$/u;
|
|
26
26
|
/** Apply a change set to a file map, returning a new map. */
|
|
27
27
|
export function applyPageChanges(files, changes) {
|
|
28
28
|
const next = { ...files };
|
|
@@ -301,9 +301,9 @@ function editRelatedText(content, oldSlug, newSlug) {
|
|
|
301
301
|
if (frontmatter === undefined)
|
|
302
302
|
return undefined;
|
|
303
303
|
const lines = frontmatter.split('\n');
|
|
304
|
-
const flowIndex = lines.findIndex(line =>
|
|
304
|
+
const flowIndex = lines.findIndex(line => parseRelatedFlow(line) !== undefined);
|
|
305
305
|
if (flowIndex !== -1) {
|
|
306
|
-
const inner =
|
|
306
|
+
const inner = parseRelatedFlow(lines[flowIndex] ?? '') ?? '';
|
|
307
307
|
const items = inner.split(',').map(item => item.trim());
|
|
308
308
|
const mapped = dedupe(items.map(item => (stripQuotes(item) === oldSlug ? swapQuoted(item, newSlug) : item)));
|
|
309
309
|
lines[flowIndex] = `related: [${mapped.join(', ')}]`;
|
package/dist/wiki-ingest.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export interface BuildWikiIngestChangesParams {
|
|
|
31
31
|
trackedMetadataAvailable?: boolean;
|
|
32
32
|
/** System-owned correction state used for post-regeneration survival verification. */
|
|
33
33
|
corrections?: CorrectionsFile;
|
|
34
|
+
/**
|
|
35
|
+
* Log entries an agent appended to its own `knowledge/log.md`, derived by the caller. Appended to
|
|
36
|
+
* the existing log, before this ingest's own summary entry.
|
|
37
|
+
*/
|
|
38
|
+
agentLogEntries?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Pages an agent deleted in a handoff, already removed from `existingFiles`. They join the result's
|
|
41
|
+
* `deletedPaths`, and a call with only deletions needs no page updates.
|
|
42
|
+
*/
|
|
43
|
+
handoffDeletedPaths?: readonly string[];
|
|
34
44
|
}
|
|
35
45
|
export interface BuildWikiIngestChangesResult {
|
|
36
46
|
files: Record<string, string>;
|
|
@@ -38,9 +48,9 @@ export interface BuildWikiIngestChangesResult {
|
|
|
38
48
|
findings: readonly WikiLintFinding[];
|
|
39
49
|
}
|
|
40
50
|
export interface WikiIngestWarning {
|
|
41
|
-
code: 'duplicate-repo-identity' | 'repos-metadata-unavailable';
|
|
51
|
+
code: 'duplicate-repo-identity' | 'repos-metadata-unavailable' | 'agent-log-entries-dropped';
|
|
42
52
|
node_ids?: string[];
|
|
43
|
-
reason?: 'read-failed' | 'parse-failed';
|
|
53
|
+
reason?: 'read-failed' | 'parse-failed' | 'not-append-only';
|
|
44
54
|
slugs?: string[];
|
|
45
55
|
}
|
|
46
56
|
export interface TrackedRepoNodeIdsResult {
|
|
@@ -65,6 +75,21 @@ export interface CommitWikiChangesParams {
|
|
|
65
75
|
* after that read are caught.
|
|
66
76
|
*/
|
|
67
77
|
baseCommitSha?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A second, per-path base for a write whose pages were edited by an agent job in an earlier job.
|
|
80
|
+
* `agentCommitSha` is the data commit that job synced; `paths` are the pages it changed or deleted
|
|
81
|
+
* (`knowledge/log.md` and `knowledge/index.md` are ignored here: the writer rebuilds both itself).
|
|
82
|
+
*
|
|
83
|
+
* Before any write, the commit must resolve and be an ancestor of the branch head, or the call fails
|
|
84
|
+
* with `HANDOFF_BASE_INVALID`. Every attempt then compares each path on the head it reads with the
|
|
85
|
+
* same path in that commit's tree, ahead of the `baseCommitSha` check, and fails with
|
|
86
|
+
* `HANDOFF_PAGE_CHANGED` if any differs: another writer edited or deleted the page since the agent
|
|
87
|
+
* read it.
|
|
88
|
+
*/
|
|
89
|
+
handoff?: {
|
|
90
|
+
agentCommitSha: string;
|
|
91
|
+
paths: readonly string[];
|
|
92
|
+
};
|
|
68
93
|
octokit?: OctokitClient;
|
|
69
94
|
maxRetries?: number;
|
|
70
95
|
/**
|
|
@@ -82,13 +107,15 @@ export interface RunWikiIngestCliDependencies {
|
|
|
82
107
|
readonly readCorrections?: typeof readCorrections;
|
|
83
108
|
readonly commitWikiChanges?: typeof commitWikiChanges;
|
|
84
109
|
readonly getChangedWikiPaths?: typeof getChangedWikiPaths;
|
|
110
|
+
/** Client for reading the agent's base commit; defaults to one built from `GITHUB_TOKEN`. */
|
|
111
|
+
readonly octokit?: OctokitClient;
|
|
85
112
|
}
|
|
86
113
|
/**
|
|
87
114
|
* Narrow Octokit client type derived from the real `@octokit/rest` SDK.
|
|
88
115
|
* See commit-metadata.ts for the rationale behind deriving rather than handwriting.
|
|
89
116
|
*/
|
|
90
117
|
export type OctokitClient = Octokit;
|
|
91
|
-
export type WikiIngestErrorCode = 'INVALID_PAYLOAD' | 'INVALID_PAGE_PATH' | 'INVALID_FRONTMATTER' | 'INVALID_WIKILINK' | 'CORRECTION_ERODED' | 'INVALID_RETRIES' | 'PROTECTED_BRANCH' | 'MISSING_TOKEN' | 'OCTOKIT_LOAD_FAILED' | 'CONFLICT_EXHAUSTED';
|
|
118
|
+
export type WikiIngestErrorCode = 'INVALID_PAYLOAD' | 'INVALID_PAGE_PATH' | 'INVALID_FRONTMATTER' | 'INVALID_WIKILINK' | 'CORRECTION_ERODED' | 'INVALID_RETRIES' | 'PROTECTED_BRANCH' | 'MISSING_TOKEN' | 'OCTOKIT_LOAD_FAILED' | 'CONFLICT_EXHAUSTED' | 'HANDOFF_BASE_INVALID' | 'HANDOFF_PAGE_CHANGED';
|
|
92
119
|
export declare class WikiIngestError extends Error {
|
|
93
120
|
readonly code: WikiIngestErrorCode;
|
|
94
121
|
readonly remediation: string;
|
|
@@ -100,6 +127,8 @@ export declare class WikiIngestError extends Error {
|
|
|
100
127
|
findings?: readonly WikiLintFinding[];
|
|
101
128
|
});
|
|
102
129
|
}
|
|
130
|
+
/** The two wiki files every writer shares. Writers rebuild both from their own sync; a handoff never supplies them. */
|
|
131
|
+
export declare function isSharedWikiFile(path: string): boolean;
|
|
103
132
|
export declare function buildWikiIngestChanges(params: BuildWikiIngestChangesParams): BuildWikiIngestChangesResult;
|
|
104
133
|
/**
|
|
105
134
|
* Commits `params.files` (and removes `params.deletedPaths`) as one commit on the data branch.
|
|
@@ -109,6 +138,9 @@ export declare function buildWikiIngestChanges(params: BuildWikiIngestChangesPar
|
|
|
109
138
|
* when it is omitted), another writer changed it, so the write fails with `CONFLICT_EXHAUSTED` and
|
|
110
139
|
* nothing is written. A ref race (`updateRef` 409, or 422 not-a-fast-forward) re-reads the head and
|
|
111
140
|
* replays the same files on it, up to `maxRetries` attempts, under the same guard.
|
|
141
|
+
*
|
|
142
|
+
* With `handoff`, the handed-off paths are checked first on every attempt against the agent's data
|
|
143
|
+
* commit (`HANDOFF_PAGE_CHANGED`), so a page conflict is never reported as a ref race.
|
|
112
144
|
*/
|
|
113
145
|
export declare function commitWikiChanges(params: CommitWikiChangesParams): Promise<CommitWikiChangesResult>;
|
|
114
146
|
export declare function validateWikilinks(files: Record<string, string>): void;
|
|
@@ -182,6 +214,12 @@ export declare function parsePorcelainPaths(stdout: string): string[];
|
|
|
182
214
|
* the four category directories.
|
|
183
215
|
*/
|
|
184
216
|
export declare function countWikiPages(paths: string[]): number;
|
|
217
|
+
/**
|
|
218
|
+
* The log text an agent appended: what its `knowledge/log.md` adds after `knowledge/log.md` at the
|
|
219
|
+
* commit it started from. `undefined` when the agent changed anything before that point, or the base
|
|
220
|
+
* log is empty, so there is nothing safe to take. Both inputs are normalized first (LF, one final newline).
|
|
221
|
+
*/
|
|
222
|
+
export declare function deriveAgentLogEntries(baseLog: string, handoffLog: string): string | undefined;
|
|
185
223
|
export declare function runWikiIngestCli(dependencies?: RunWikiIngestCliDependencies): Promise<void>;
|
|
186
224
|
type ReadUtf8File = (path: string, encoding: 'utf8') => Promise<string>;
|
|
187
225
|
export declare function loadTrackedRepoNodeIds(readFileImpl?: ReadUtf8File): Promise<TrackedRepoNodeIdsResult>;
|