mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
* fix(desktop): keep a selected folder that is named like a database log A folder the caller selected stays a selected root when its name ends in -journal or -wal. A generated log beside a database file still does not. * fix(desktop): refuse a build command that exceeds the argument vector Splitting one argument still leaves the whole command under Linux's E2BIG limit. A request past that budget is refused before the process starts. * fix(desktop): remap a captured path in an env assignment The program word stays the program. A NAME=value assignment keeps its name and follows a captured path value when that path's spelling moves. * fix(desktop): keep oversized text out of draft guards A value or line longer than the draft's text bound is not copied into a guard, so one large frame cannot fill the task by itself. * docs(desktop): correct dconf and check-preview guidance A dconf version kept without its bytes is unreadable, not an empty key list. A values preview is copied into a check only when it is an uncut scalar. * fix(desktop): keep an empty env value empty An empty assignment is not a path. Resolving it named the working directory and rewrote the value when that directory's spelling moved. * fix(desktop): keep a run's font on the run, not the frame's text A formatting attribute such as Scribus FONT is not the document's words. Joining it across runs made a frame-wide text check of ItalicBold that failed when a run was split. The font stays a value check on its own run. * fix(desktop): do not pair JSON records by a selector-shaped field A field named @record_id matched the id suffix and became [@record_id=one], which reads an XML attribute. Records paired while their pointers matched nothing. Names that start with @ or > are not id fields. * fix(desktop): count a save-numbered id once when pairing members xml:id and draw:id values such as id123456 were both generated and numbered, so the same selector was counted twice and refused. A text edit of two elements keyed only by those ids now keeps its check. * fix(desktop): count members of root JSON arrays * fix(desktop): preserve selected state directory aliases * docs(desktop): explain tool and skill refresh prerequisites * fix(desktop): align nested state selections with aliases * fix(desktop): check build runtime export capability * fix(desktop): provide complete runtime repack command * fix(desktop): retain runtime probe failures * test(desktop): identify capture independently of schema preflight * fix(desktop): preserve document URLs and value formats * fix(desktop): bound added checks and omit noisy text guards * fix(desktop): exclude unseeded locks from task checks * fix(desktop): preserve draft bounds and launch URLs * fix(desktop): render draft values using their source format * fix(desktop): preserve launch URLs and JSON draft values * fix(desktop): recognize captured file URL documents * fix(desktop): render captured file URLs for runtime home * fix(desktop): distinguish phone numbers from save metadata * fix(desktop): preserve launch URLs and user text * fix(desktop): preserve captured URLs and user text * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): render captured file URLs at launch * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): retain clean text in noisy record fields * fix(desktop): classify added line values in one pass * fix(desktop): apply XML quote rules to text drafts * fix(desktop): distinguish XML attributes from quoted text * fix(desktop): preserve parsed XML line context * Format appTaskChecks.ts with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): retain partial XML syntax facts * fix(desktop): recognize markdown autolinks in drafts * fix(desktop): reuse export exclusions in task drafts * docs(desktop): clarify gzip scrub limits * style(desktop): format the noisy-record draft files with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): give the root-array draft test its own describe The records describe grew past the 150-line lint limit with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): move launch execution to boundary suite * fix(desktop): redact build schema failure output * refactor(desktop): normalize schema failure once * fix(desktop): omit unsafe schema parser snippets * fix(desktop): preserve draft change and selected path evidence * test(desktop): group retained dconf note regressions * fix(desktop): respect archive entry format in draft checks * test(desktop): separate unowned-file draft note cases * fix(desktop): preserve explicitly selected journal documents * fix(xml): avoid scanning selected ancestors per tag * fix(desktop): exclude covered text runs from draft counts * fix: preserve text when drafting desktop checks * fix: restore nested desktop state aliases * refactor: share UUID recognition across draft checks * fix: retain multiline XML attribute context * fix: retain relationship identifiers in task drafts * docs: clarify desktop draft identifier handling * test(desktop): type formatting run cases as pairs * fix(desktop): carry document format into selector evaluation * test(desktop): separate markup boundary regression group * fix(desktop): propagate formats through draft selectors * fix(desktop): switch selector format within archive entries * test(desktop): remove duplicate seed filename case * test(desktop): pass JSON format in exported index parity * docs(desktop): clarify task checks before scratch export * fix(desktop): respell selected paths in file URLs * test(desktop): group captured document spelling cases * fix(desktop): bound draft preview serialization * fix(desktop): restore selected state aliases * fix(desktop): respect entry format in draft noise * test(desktop): match exported alias evidence at its JSON boundary * fix(desktop): retain app state logs in exported seeds * fix: align desktop numeric seed comparisons * test(desktop): keep one draft exclusion fixture import * test(desktop): retain log eligibility across draft test groups * style: fix desktop launch test formatting * style: format native command path schema * fix(desktop): map selected external launch paths * style(desktop): format seed document regressions * test(desktop): group launch URL regressions * fix(desktop): omit redundant nested state aliases * fix(desktop): compare added free text as a whole * fix(desktop): check recompressed seeds as shipped * docs(desktop): clarify recovered seed format * test(desktop): group shipped gzip seed checks * fix: retain quoted identifier text in task drafts * fix: recognize Unicode XML in desktop state readers * fix(desktop): bound nested draft checks * fix(desktop): explain missing arrays in truncated views * fix(desktop): find XML type references in archives * test(desktop): remove duplicated capture assertions * fix(desktop): retain nested omitted text boundaries * style(desktop): format draft value imports * test(desktop): group model and draft value checks * fix(desktop): map captured launch paths once * refactor: remove unused whole-text guard state * refactor: stop passing unused text children * test: align free-text checks with whole-note policy * fix: validate recompressed desktop archive seeds * style(desktop): format archive seed regression * fix(desktop): retain placed-item sibling guards * test(desktop): specify XML format in heap pairing fixture * fix(desktop): redact runtime schema errors * fix(desktop): normalize captured file URLs * fix(desktop): report seeded files by capture path * fix(desktop): retain startup readiness failure stage * fix(desktop): retain JSON date checks * fix(desktop): preserve archive formatting checks * fix(desktop): preserve explicitly selected journal files * fix(desktop): report build command transport limits * docs(desktop): clarify unreadable dconf versions * test(desktop): remove duplicate history regression * fix(desktop): align XML names with exported reader * fix(desktop): retain record identity in draft checks * fix(desktop): scrub XML independently of reader compatibility * style(desktop): format review fixes * style(desktop): format capture review fixes --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
8.4 KiB
TypeScript
243 lines
8.4 KiB
TypeScript
import type { JsonValue } from "../jsonValue.js";
|
|
import type { DesktopTaskCheckInput } from "./appTask.js";
|
|
import {
|
|
isBookkeepingName,
|
|
isStamp,
|
|
isVolatileValue,
|
|
} from "./appTaskDraftNoise.js";
|
|
import { valueEqualsCheck } from "./appTaskDraftScalars.js";
|
|
import {
|
|
isGeneratedId,
|
|
isIdentifyingAttribute,
|
|
isPlaceholder,
|
|
isViewStateName,
|
|
rankAttributes,
|
|
} from "./attributeRank.js";
|
|
import { escapeToken, isXmlElement, resolvePointer } from "./viewPointer.js";
|
|
import { spellCondition } from "./viewSelector.js";
|
|
import { checkFits, type AddedContext } from "./appTaskDraftAdded.js";
|
|
|
|
type Element = Record<string, JsonValue> & {
|
|
name: string;
|
|
attributes: Record<string, JsonValue>;
|
|
children: JsonValue[];
|
|
};
|
|
|
|
/** Checks of the records nested in one element added whole, and of one record's attributes. */
|
|
const MAX_NESTED_CHECKS = 6;
|
|
const MAX_PER_RECORD = 3;
|
|
|
|
/** Whether an element is a record of attributes alone, as KMyMoney's `<SPLIT value="-1749/20" account="A000001" …/>`: no element or text of its own. */
|
|
const attributesOnly = (item: JsonValue): item is Element =>
|
|
isXmlElement(item) &&
|
|
Object.keys(item.attributes).length > 0 &&
|
|
item.children.every(
|
|
(child) => typeof child === "string" && child.trim() === "",
|
|
);
|
|
|
|
/** The records of attributes alone among an element's children, when they are all one tag; empty otherwise. */
|
|
const recordsIn = (element: JsonValue | undefined): Element[] => {
|
|
const items = isXmlElement(element)
|
|
? element.children.filter(isXmlElement)
|
|
: [];
|
|
const records = items.filter(attributesOnly);
|
|
return records.length === items.length &&
|
|
records.every((record) => record.name === records[0]?.name)
|
|
? records
|
|
: [];
|
|
};
|
|
|
|
/** A list of records of attributes alone in an element: the steps from it to the element holding them, none for its own or a child's tag for a child's, as a transaction's `<SPLITS>`, and the records. */
|
|
interface RecordList {
|
|
readonly steps: readonly string[];
|
|
readonly records: readonly Element[];
|
|
}
|
|
|
|
/**
|
|
* The lists of records of attributes alone an element holds: its own
|
|
* children, or those of a child holding nothing else that its tag names
|
|
* alone, as a KMyMoney `<TRANSACTION>` its `<SPLITS>` of `<SPLIT>`.
|
|
*/
|
|
export const recordListsOf = (element: JsonValue): RecordList[] => {
|
|
const own = recordsIn(element);
|
|
if (own.length > 0) return [{ steps: [], records: own }];
|
|
const children = isXmlElement(element)
|
|
? element.children.filter(isXmlElement)
|
|
: [];
|
|
const tags = new Map<string, number>();
|
|
for (const { name } of children) tags.set(name, (tags.get(name) ?? 0) + 1);
|
|
return children.flatMap((child) => {
|
|
const records = recordsIn(child);
|
|
return records.length > 0 &&
|
|
tags.get(child.name) === 1 &&
|
|
!/[=[\]]/u.test(child.name)
|
|
? [{ steps: ["children", escapeToken(`[name=${child.name}]`)], records }]
|
|
: [];
|
|
});
|
|
};
|
|
|
|
/** The records at a list's steps in each like sibling of an element, as the splits of the transactions already in the book. */
|
|
const cousinsOf = (
|
|
likes: readonly JsonValue[],
|
|
steps: readonly string[],
|
|
tag: string,
|
|
): Element[] =>
|
|
likes.flatMap((like) =>
|
|
recordsIn(
|
|
steps.length === 0
|
|
? like
|
|
: resolvePointer(like, `/${steps.join("/")}`, "xml"),
|
|
).filter((record) => record.name === tag),
|
|
);
|
|
|
|
/** Whether a value a selector can carry and that names something across saves. */
|
|
const selectable = (key: string, value: JsonValue | undefined) =>
|
|
typeof value === "string" &&
|
|
value !== "" &&
|
|
!/[=[\]]/u.test(key) &&
|
|
!value.includes("][") &&
|
|
!isStamp(value) &&
|
|
!isPlaceholder(value) &&
|
|
!isVolatileValue(value, isBookkeepingName(key));
|
|
|
|
/**
|
|
* The selector naming a nested record alone among its list's: an
|
|
* identifier first, unless it is one the app generates or numbers anew in
|
|
* each record holding such a list, as KMyMoney's `S0001` in every
|
|
* transaction; then the best ranked attribute short of a coordinate or an
|
|
* amount, as a split's `[@account=A000001]`.
|
|
*/
|
|
const recordKey = (
|
|
record: Element,
|
|
records: readonly Element[],
|
|
cousins: readonly Element[],
|
|
chars: number,
|
|
) => {
|
|
const values = Object.entries(record.attributes).flatMap(([key, value]) =>
|
|
selectable(key, value) && typeof value === "string" && value.length <= chars
|
|
? [{ key, value }]
|
|
: [],
|
|
);
|
|
const repeated = (key: string, value: string) =>
|
|
cousins.some((cousin) => cousin.attributes[key] === value);
|
|
const [best] = [
|
|
...values.filter(
|
|
({ key, value }) =>
|
|
isIdentifyingAttribute(key) &&
|
|
!isGeneratedId(value) &&
|
|
!repeated(key, value),
|
|
),
|
|
...rankAttributes(
|
|
values.filter(({ key }) => !isIdentifyingAttribute(key)),
|
|
).filter(({ tier }) => tier < 3),
|
|
].filter(
|
|
({ key, value }) =>
|
|
records.filter((other) => other.attributes[key] === value).length === 1,
|
|
);
|
|
return best === undefined
|
|
? undefined
|
|
: {
|
|
key: best.key,
|
|
segment: escapeToken(
|
|
`[name=${record.name}]${spellCondition({ prefix: "@", ...best })}`,
|
|
),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* The keys of a record checked one by one: those whose values vary among
|
|
* the like records, its list's and those under the element's like siblings,
|
|
* as a split's `value` and `payee`, but not its `memo` or `price` that every
|
|
* split holds alike, nor one each like record holds equal to an earlier one,
|
|
* as `shares` repeats `value`; never its selector's, an identifier, a time,
|
|
* a volatile value, view state or an id the app renumbers.
|
|
*/
|
|
const checkedKeys = (
|
|
record: Element,
|
|
likes: readonly Element[],
|
|
skip: string,
|
|
) => {
|
|
const keys: string[] = [];
|
|
for (const [key, value] of Object.entries(record.attributes)) {
|
|
const all = likes.map((like) => like.attributes[key]);
|
|
if (
|
|
key === skip ||
|
|
typeof value !== "string" ||
|
|
isIdentifyingAttribute(key) ||
|
|
isPlaceholder(value) ||
|
|
isStamp(value) ||
|
|
isVolatileValue(value, isBookkeepingName(key)) ||
|
|
isViewStateName(key) ||
|
|
new Set(all).size < 2 ||
|
|
keys.some((earlier) =>
|
|
likes.every(
|
|
(like) => like.attributes[earlier] === like.attributes[key],
|
|
),
|
|
)
|
|
)
|
|
continue;
|
|
keys.push(key);
|
|
}
|
|
return keys;
|
|
};
|
|
|
|
/** What checks of nested records read: the file, the end, and whether a pointer names its value without a step by position. */
|
|
export interface NestedContext extends Pick<AddedContext, "chars" | "long"> {
|
|
readonly file: string;
|
|
readonly final: JsonValue | null;
|
|
readonly named: (pointer: string) => boolean;
|
|
}
|
|
|
|
/**
|
|
* A `value_equals` per attribute of each record of attributes alone nested
|
|
* in an element added whole to a list, as each split of a KMyMoney
|
|
* transaction holds its amount, account and payee, which the element's own
|
|
* attributes and text leave out: the record named by `recordKey`, its
|
|
* attributes by `checkedKeys`, at most six in all. Each fails at the seed,
|
|
* which lacks the element. Empty for an element with no like sibling, as no
|
|
* record then tells what varies.
|
|
*/
|
|
export const nestedRecordChecks = (
|
|
context: NestedContext,
|
|
pointer: string,
|
|
element: JsonValue,
|
|
): DesktopTaskCheckInput[] => {
|
|
if (!isXmlElement(element)) return [];
|
|
const list = resolvePointer(
|
|
context.final,
|
|
pointer.replace(/\/[^/]+$/u, ""),
|
|
"xml",
|
|
);
|
|
const likes = (Array.isArray(list) ? list : []).filter(
|
|
(item) =>
|
|
item !== element && isXmlElement(item) && item.name === element.name,
|
|
);
|
|
if (likes.length === 0) return [];
|
|
return recordListsOf(element)
|
|
.flatMap(({ steps, records }) => {
|
|
const cousins = cousinsOf(likes, steps, records[0]?.name ?? "");
|
|
return records.flatMap((record) => {
|
|
const key = recordKey(record, records, cousins, context.chars);
|
|
if (key === undefined) return [];
|
|
const at = [pointer, ...steps, "children", key.segment].join("/");
|
|
return checkedKeys(record, [...records, ...cousins], key.key)
|
|
.filter((name) => {
|
|
const value = record.attributes[name];
|
|
return (
|
|
typeof value === "string" &&
|
|
checkFits(context, `${at}/attributes/${escapeToken(name)}`, value)
|
|
);
|
|
})
|
|
.slice(0, MAX_PER_RECORD)
|
|
.flatMap((name) => {
|
|
const field = `${at}/attributes/${escapeToken(name)}`;
|
|
const value = record.attributes[name];
|
|
return context.named(field) && typeof value === "string"
|
|
? [valueEqualsCheck(context.file, field, value)]
|
|
: [];
|
|
});
|
|
});
|
|
})
|
|
.slice(0, MAX_NESTED_CHECKS);
|
|
};
|