mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
* docs(specs): plan a template rule that shares templated positions Record the outcome, requirements, non-goals, plan and tasks for prefix-variance, an opt-in environment-manifest rule that templates a literal segment with the parameter another tool already uses at that position, so a session's writes to one entity share the read's template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(analysis-view): share templated positions across manifest tools Add the opt-in prefix-variance template rule. It runs after the other opt-in rules and templates a literal at a position that another tool, of any method or segment count, templates by one of those rules after the same preceding parts, when the literal is one of that parameter's observed values or carries a digit and is spelled like one. The parameter takes the shared parameter's name, tools that now match merge, and a merged position keeps a tool's own rule when one templated it. A write to the one entity a session edited, such as PATCH /api/v1/collections/0RZ576QZQA5KG, becomes patch_collections_by_collection_id on the read's template instead of a literal single-entity tool. Tool naming moves to its own module, and the three comparing passes share the 32-segment split. Manifests that do not list the rule are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(analysis-view): drop the unused ManifestRule alias Parameters now type their rule as a format rule, a shared opt-in rule or prefix-variance, so nothing reads the union alias. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(specs): record the prefix-variance checks and corpus results Record the gates, and the 50 showcase captures with and without the rule: identical output without it, 25 write and 34 read tools templated with it, and the write tools the write-plan branch serves on top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(analysis-view): share positions with digit-free generated identifiers prefix-variance accepted an unobserved literal only when it carried a digit, so a generated identifier that happened to hold none stayed a literal tool: an n8n workflow id such as FkMwxmXNGaGVsXFG, whose reads then never joined the item view. A literal now also qualifies when every value observed at the position has one length of eight or more characters and the literal has that length and uses only their character classes. Across the 50 showcase captures this templates 10 more read tools in Memos and n8n and no words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(specs): record five environments rebuilt end to end with the rule Five randomly drawn environments rebuilt on VMs with the rule and the write-plan branch: smoke, retrieval and write tasks for two models, and the same calls against the live applications. Record the results, the misses and their causes, and the corpus numbers after the fixed-length clause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(analysis-view): share a position with the parameter that observed the literal A shared position folded every donor's values into the earliest donor's parameter. With an opaque-token read before sibling-variance reads at the same position, a PATCH of /api/items/b2 qualified on the sibling's evidence but became /api/items/{uid} instead of joining the item read's /api/items/{item_id}. Each position now keeps one donor per naming rule with its own values, and a literal takes the donor that observed it, else the earliest one it is spelled like. Each donor's spelling (length and character classes of its values, and its fixed-length family) is summarised once per position, so a literal is checked in time independent of how many values were observed, rather than copying and rescanning them for every candidate. A family now forms from one observed value, as the requirement says, so a digit-free id of the same length shares a position that only one opaque-token or after-uid-literal tool supplied. The 50 showcase captures give the same tools as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(specs): record the prefix-variance review round Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(analysis-view): bound the spelling summary of shared positions spellingSummary classified every observed value with three full-size replaceAll copies and a set per distinct character, so a donor holding a multi-megabyte segment cost unaccounted memory and time in finish(). Characters are now classified in one pass, and a value longer than 128 characters takes no part in spelling comparisons: it shares a position only as an exactly observed value. The 50 showcase captures give the same tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
import {
|
|
compareUnicodeCodePoints as compare,
|
|
compositeKey,
|
|
} from "../unicodeCodePointOrder.js";
|
|
import { VERSION_TOKEN, type RoutePart } from "./environmentManifestRules.js";
|
|
import { ROUTE_RULES } from "./webNetworkRouteTemplates.js";
|
|
|
|
const DROPPED_NAME_LITERALS = new Set(["api", "apis", "namespaces", "default"]);
|
|
const TOOL_NAME_LIMIT = 60;
|
|
const BASE_RULES: ReadonlySet<string> = new Set(ROUTE_RULES);
|
|
|
|
const sanitizeToken = (text: string): string =>
|
|
text
|
|
.toLowerCase()
|
|
.replaceAll(/[^a-z0-9_]+/gu, "_")
|
|
.replaceAll(/_+/gu, "_")
|
|
.replace(/^_/u, "")
|
|
.replace(/_$/u, "");
|
|
|
|
const singular = (word: string): string => {
|
|
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
|
|
return word;
|
|
};
|
|
|
|
const paramBaseName = (parts: readonly RoutePart[], index: number): string => {
|
|
const part = parts[index];
|
|
if (part?.kind !== "param") return "";
|
|
// A prefix-variance parameter is named as the parameter it shares a position with.
|
|
switch (part.rule === "prefix-variance" ? part.sharedRule : part.rule) {
|
|
case "numeric":
|
|
return "id";
|
|
case "uuid":
|
|
return "uuid";
|
|
case "hex":
|
|
return "hash";
|
|
case "iso-date":
|
|
return "date";
|
|
case "after-uid-literal":
|
|
case "opaque-token":
|
|
return "uid";
|
|
case "sibling-variance": {
|
|
const previous = parts[index - 1];
|
|
const stem =
|
|
previous?.kind === "literal"
|
|
? sanitizeToken(singular(previous.value))
|
|
: "";
|
|
return stem.length > 0 ? `${stem}_id` : "id";
|
|
}
|
|
case "sibling-pair-variance": {
|
|
// The role was fixed when the pair formed; the plural literal precedes the pair's first half.
|
|
const role = part.pairRole ?? "namespace";
|
|
const plural = parts[role === "name" ? index - 2 : index - 1];
|
|
const stem =
|
|
plural?.kind === "literal" ? sanitizeToken(singular(plural.value)) : "";
|
|
return stem.length > 0 ? `${stem}_${role}` : role;
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Parameter names in segment order; repeated names get numeric suffixes. */
|
|
export const parameterNames = (parts: readonly RoutePart[]): string[] => {
|
|
const used = new Map<string, number>();
|
|
return parts.flatMap((part, index) => {
|
|
if (part.kind !== "param") return [];
|
|
const base = paramBaseName(parts, index);
|
|
const count = used.get(base) ?? 0;
|
|
used.set(base, count + 1);
|
|
return [count === 0 ? base : `${base}_${String(count + 1)}`];
|
|
});
|
|
};
|
|
|
|
/** Format-rule placeholders keep their api-surface spelling; opt-in rules use the parameter name. */
|
|
export const pathTemplate = (
|
|
parts: readonly RoutePart[],
|
|
names: readonly string[],
|
|
): string => {
|
|
let next = 0;
|
|
return `/${parts
|
|
.map((part) => {
|
|
if (part.kind === "literal") return part.value;
|
|
const name = names[next] ?? "";
|
|
next += 1;
|
|
return BASE_RULES.has(part.rule) ? `{${part.rule}}` : `{${name}}`;
|
|
})
|
|
.join("/")}`;
|
|
};
|
|
|
|
/** Names assigned in one manifest and the next suffix to try for each colliding truncated base. */
|
|
export interface ToolNameRegistry {
|
|
readonly used: Set<string>;
|
|
readonly nextSuffix: Map<string, number>;
|
|
}
|
|
export const toolNameRegistry = (): ToolNameRegistry => ({
|
|
used: new Set(),
|
|
nextSuffix: new Map(),
|
|
});
|
|
/** Deterministic tool name from the method and route literals, unique within a manifest. */
|
|
export const toolName = (
|
|
method: string,
|
|
parts: readonly RoutePart[],
|
|
names: readonly string[],
|
|
registry: ToolNameRegistry,
|
|
): string => {
|
|
const literals = parts.flatMap((part, index) => {
|
|
if (part.kind !== "literal") return [];
|
|
const value = part.value;
|
|
if (
|
|
value.length === 0 ||
|
|
DROPPED_NAME_LITERALS.has(value) ||
|
|
VERSION_TOKEN.test(value) ||
|
|
(value === "uid" && parts[index + 1]?.kind === "param")
|
|
)
|
|
return [];
|
|
return [value.split(".")[0] ?? ""];
|
|
});
|
|
const base =
|
|
sanitizeToken(
|
|
[
|
|
method.toLowerCase(),
|
|
...literals,
|
|
...(names.length > 0 ? ["by", ...names] : []),
|
|
].join("_"),
|
|
) || "tool";
|
|
const truncate = (text: string, limit: number): string =>
|
|
text.slice(0, limit).replace(/_+$/u, "");
|
|
let candidate = truncate(base, TOOL_NAME_LIMIT);
|
|
if (registry.used.has(candidate)) {
|
|
let suffix = 2;
|
|
for (;;) {
|
|
const tail = `_${String(suffix)}`;
|
|
const prefix = truncate(base, TOOL_NAME_LIMIT - tail.length);
|
|
// Suffixes can truncate distinct full names to the same prefix. Share
|
|
// progress for that actual candidate family, separately for each width:
|
|
// a longer suffix can change the prefix and must not skip free short names.
|
|
const namespace = compositeKey([prefix, tail.length]);
|
|
const next = registry.nextSuffix.get(namespace) ?? suffix;
|
|
if (next > suffix) {
|
|
suffix = next;
|
|
continue;
|
|
}
|
|
candidate = `${prefix}${tail}`;
|
|
registry.nextSuffix.set(namespace, suffix + 1);
|
|
if (!registry.used.has(candidate)) break;
|
|
suffix += 1;
|
|
}
|
|
}
|
|
registry.used.add(candidate);
|
|
return candidate;
|
|
};
|
|
|
|
/** Sort tools by template then method with locale-independent ordering. */
|
|
export const compareTools = (
|
|
left: { readonly template: string; readonly method: string },
|
|
right: { readonly template: string; readonly method: string },
|
|
): number =>
|
|
compare(left.template, right.template) || compare(left.method, right.method);
|