Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
fa127c5
refactor: clear the avoidable ratchet findings of pack 0.2.17 (#59)
Upd4ting Oct 30, 2026
ed7f23c
fix(metadata): look up filter and index names by own key (#62)
Upd4ting Oct 31, 2026
3fedad4
fix(list): answer 400 for an unknown comparison in the default filter…
Upd4ting Oct 31, 2026
4dad937
docs: typecheck every example and drop hand-written version prose (#63)
Upd4ting Oct 31, 2026
6eca4ec
build(deps): prove the package against interface-database 0.3.0 (#64)
Upd4ting Oct 31, 2026
86c8420
build(deps): prove interface-data-api against interface-database-deco…
Upd4ting Oct 31, 2026
1c5030c
feat(data-api)!: type the filter, computed, option and controller con…
Upd4ting Oct 31, 2026
4352b2c
build(deps): prove interface-data-api against the decorators' row for…
Upd4ting Oct 31, 2026
e070488
fix(data-api): refuse a written row holding a value no database store…
Upd4ting Oct 31, 2026
8574009
refactor(data-api): register data routes with their controller class …
Upd4ting Nov 1, 2026
e070b68
feat(data-api)!: type the default of a computed field as a field valu…
Upd4ting Nov 1, 2026
2748287
build(deps): re-pin interface-database-decorators to the post-#70 tar…
Upd4ting Nov 1, 2026
26c36e9
feat(data-api)!: type a data route's parameters by the tuple its args…
Upd4ting Nov 1, 2026
d7fa275
test(data-api): read the exports of a second instance as unknown and …
Upd4ting Nov 1, 2026
3e63a94
test(data-api): type ParseBody fixtures and mark off-contract bodies …
Upd4ting Nov 1, 2026
2d14b76
refactor(interface-data-api): register data routes with their control…
Upd4ting Nov 1, 2026
d5c10f0
fix(interface-data-api)!: require interface-api 0.3.0 or newer (#80)
Upd4ting Nov 1, 2026
8823b07
build(deps): pin interface-api to the f2a99b5 tarball (#81)
Upd4ting Nov 1, 2026
169448e
chore(guardrails): lower ratchet ceilings to measured (#83)
Upd4ting Nov 1, 2026
eccde48
build(deps): re-pin interface-api-util 0.4.0 to 639f38b (#82)
Upd4ting Nov 1, 2026
d20fad6
refactor(interface-data-api): build on interface-core 0.4's typed dec…
Upd4ting Nov 2, 2026
65afb35
chore(ratchet): delete the interface-data-api guardrail ratchet (#85)
Upd4ting Nov 2, 2026
354dfaf
refactor(controller): read controller fields through fieldsOf (#86)
Upd4ting Nov 2, 2026
6293a02
chore: merge main into campaign/integration (#88)
Upd4ting Nov 2, 2026
c9695f5
docs: pass the data-api docs to the docs standard (#89)
Upd4ting Nov 3, 2026
eed94e2
feat(skills): add the verify-interface verification skill
Upd4ting Nov 3, 2026
b37d612
fix(skills): catch the foreign, route-options and modifiers breaks an…
Upd4ting Nov 3, 2026
e3cb9e9
feat(skills): cover guard, new, edit, filter and list-mode behaviours…
Upd4ting Nov 4, 2026
3202e20
feat(skills): sweep the string options and per-action overrides and f…
Upd4ting Nov 4, 2026
44ef9f1
feat(skills): cover derived controllers, foreign target metadata, dec…
Upd4ting Nov 4, 2026
74e45ac
feat(skills): cover empty forms, mandatory order, decorator stacking …
Upd4ting Nov 4, 2026
7fd6409
chore: merge main into verify skill branch
Upd4ting Nov 5, 2026
a91c1c0
chore: sync AGENTS.md with the verify skill
Upd4ting Nov 5, 2026
2c98aab
test(verify): add the formal-field-access check
Upd4ting Nov 5, 2026
48925b1
test(verify): map every src file to the features whose journeys check it
Upd4ting Nov 5, 2026
a26065c
docs(verify): keep Validation in the formal check's left-out list
Upd4ting Nov 5, 2026
c74ad8b
docs(skills): list every journey in the feature map and every covered…
Upd4ting Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .agents/skills/formal-field-access/SKILL.md

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions .agents/skills/formal-field-access/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Controller } from "@antelopejs/interface-api";
import type { Class, DecoratorTarget } from "@antelopejs/interface-core/decorators";
import { RegisterTable, Table } from "@antelopejs/interface-database-decorators";

import { DataController, GetDataControllerMeta } from "../../../src/index.ts";
import { Access, Computed, type DataAPIMeta, Joined, Listable } from "../../../src/metadata.ts";
import { type Case, type ClassDecl, classNames, type Declaration, type Op } from "./cases.ts";

const SCHEMA = "formal-field-access";
const JOINED_TABLE = "formal-field-access-other";

class Row extends Table {}
RegisterTable("formal-field-access-rows", SCHEMA)(Row);

class Other extends Table {}
RegisterTable(JOINED_TABLE, SCHEMA)(Other);

/** One accessor call the check's getters and setters record: which class's accessor ran, on which field, with what. */
export interface AccessorCall {
owner: string;
field: string;
kind: "get" | "set";
value?: string;
}

/** A controller class the check built, with its metadata, the name it prints under and what it declares. */
export interface Built {
name: string;
cls: Class<object>;
meta: DataAPIMeta;
decl: ClassDecl;
}

/** A getter answers `get:<class>:<field>`; the class is the one whose prototype holds the accessor. */
export function getterValue(owner: string, field: string): string {
return `get:${owner}:${field}`;
}

export function usesGetter(declared: Declaration): boolean {
return declared === "getter" || declared === "accessor";
}

export function usesSetter(declared: Declaration): boolean {
return declared === "setter" || declared === "accessor";
}

/** The class defaults of a declaration: `name = "default:<name>"` for each defaulted property. */
export function defaultsOf(decl: ClassDecl): Map<string, string> {
const defaults = new Map<string, string>();
for (const [name, fieldDecl] of Object.entries(decl)) {
if (fieldDecl.declared === "defaulted") defaults.set(name, `default:${name}`);
}
return defaults;
}

function defineAccessors(
prototype: DecoratorTarget,
owner: string,
field: string,
declared: Declaration,
log: AccessorCall[],
) {
if (!usesGetter(declared) && !usesSetter(declared)) return;
const descriptor: PropertyDescriptor = { configurable: true, enumerable: false };
if (usesGetter(declared)) {
descriptor.get = () => {
log.push({ owner, field, kind: "get" });
return getterValue(owner, field);
};
}
if (usesSetter(declared)) {
descriptor.set = (value: string | undefined) => {
log.push({ owner, field, kind: "set", value });
};
}
Object.defineProperty(prototype, field, descriptor);
}

/**
* Applies one decorator as TypeScript's `__decorate` does: a property passes
* no descriptor, an accessor passes its own property descriptor.
*/
function decorate(op: Op, prototype: DecoratorTarget, field: string, descriptor: PropertyDescriptor | undefined) {
switch (op.decorator) {
case "Access": {
const decorator = Access(op.mode, op.overrides);
if (descriptor) decorator(prototype, field, descriptor);
else decorator(prototype, field);
return;
}
case "Computed":
Computed(() => 0)(prototype, field);
return;
case "Joined":
Joined({ table: JOINED_TABLE, localKey: op.localKey, remoteField: "value" })(prototype, field);
return;
case "Listable": {
const decorator = Listable(op.required, op.mode);
if (descriptor) decorator(prototype, field, descriptor);
else decorator(prototype, field);
}
}
}

/**
* Declares one controller class as a module author would: it extends
* `DataController(Row, {}, Controller(location[, parent]))`, defines its
* accessors, then runs each field's decorators bottom-up. The metadata is read
* through `GetDataControllerMeta`, as the routes read it.
*/
function declareClass(name: string, decl: ClassDecl, log: AccessorCall[], parent?: Built): Built {
const base = parent
? DataController(Row, {}, Controller(`/${name}`, parent.cls))
: DataController(Row, {}, Controller(`/${name}`));
const defaults = Object.fromEntries(defaultsOf(decl));
const cls = class extends base {
constructor() {
super();
Object.assign(this, defaults);
}
};
Object.defineProperty(cls, "name", { value: name });
for (const [field, fieldDecl] of Object.entries(decl)) {
defineAccessors(cls.prototype, name, field, fieldDecl.declared, log);
const descriptor = Object.getOwnPropertyDescriptor(cls.prototype, field);
for (const op of fieldDecl.ops) decorate(op, cls.prototype, field, descriptor);
}
return { name, cls, meta: GetDataControllerMeta(new cls()), decl };
}

/** The classes of a case in definition order, and the accessor log they share. */
export interface BuiltCase {
classes: Built[];
log: AccessorCall[];
}

/** Builds the classes of a case in definition order, sharing one accessor log. */
export function build(testCase: Case): BuiltCase {
const log: AccessorCall[] = [];
const [parentName, ...childNames] = classNames(testCase);
const parent = declareClass(parentName, testCase.parent, log);
const children = testCase.children.map((decl, index) => declareClass(childNames[index], decl, log, parent));
return { classes: [parent, ...children], log };
}
249 changes: 249 additions & 0 deletions .agents/skills/formal-field-access/cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { AccessMode } from "../../../src/metadata.ts";

/** A decorator a controller writes on a field, with the arguments the check varies. */
export type Op =
| { decorator: "Access"; mode: AccessMode; overrides?: Record<string, AccessMode> }
| { decorator: "Computed" }
| { decorator: "Joined"; localKey: string }
| { decorator: "Listable"; required: boolean | string[]; mode?: string };

/**
* How a controller class declares a field: `declare a`, `a = "default:a"`,
* `get a()`, `set a(value)`, or both accessors.
*/
export type Declaration = "property" | "defaulted" | "getter" | "setter" | "accessor";

/** A field as one class declares it, its decorators in the order they run: the bottom one first. */
export interface FieldDecl {
declared: Declaration;
ops: Op[];
}

/** The fields of one controller class, by name. */
export type ClassDecl = Record<string, FieldDecl>;

/**
* One case: a parent controller, then the derived controllers defined after
* it in order, each extending the parent. A case with no child is one
* controller on its own.
*/
export interface Case {
parent: ClassDecl;
children: ClassDecl[];
}

export type Configuration = "single" | "pair" | "derived";
export type Mode = "ci" | "full";

export const CONFIGURATIONS: Configuration[] = ["single", "pair", "derived"];

/** The database key no field declares: the row holds it, a body sends it, `@Listable([...])` and `@Joined` may name it. */
export const DB_ONLY_KEY = "k";

const { ReadOnly, WriteOnly, ReadWrite } = AccessMode;
const MODES = [ReadOnly, WriteOnly, ReadWrite];
const OVERRIDES: (Record<string, AccessMode> | undefined)[] = [
undefined,
{ get: ReadOnly },
{ get: WriteOnly },
{ new: ReadWrite },
{ edit: ReadWrite },
{ custom: ReadWrite },
{ _default: WriteOnly },
{ get: WriteOnly, new: ReadWrite },
];
const ALL_DECLARATIONS: Declaration[] = ["property", "defaulted", "getter", "setter", "accessor"];
const MAIN_DECLARATIONS: Declaration[] = ["property", "accessor"];

function access(mode: AccessMode, overrides?: Record<string, AccessMode>): Op {
return { decorator: "Access", mode, overrides };
}

/** Every decorator the check writes on a field, next to `other` (a second field or {@link DB_ONLY_KEY}). */
function fullAlphabet(other: string): Op[] {
return [
...MODES.flatMap((mode) => OVERRIDES.map((overrides) => access(mode, overrides))),
{ decorator: "Computed" },
{ decorator: "Joined", localKey: other },
{ decorator: "Listable", required: true },
{ decorator: "Listable", required: false },
{ decorator: "Listable", required: [other] },
{ decorator: "Listable", required: true, mode: "detailed" },
];
}

/** The alphabet of the longer sequences: each decorator once, plus the two override tables the docs show. */
function coreAlphabet(other: string): Op[] {
return [
access(ReadOnly),
access(WriteOnly),
access(ReadWrite),
access(ReadOnly, { new: ReadWrite }),
access(ReadWrite, { edit: ReadOnly }),
{ decorator: "Computed" },
{ decorator: "Joined", localKey: other },
{ decorator: "Listable", required: true },
{ decorator: "Listable", required: [other] },
];
}

/** Every sequence of `min` to `max` symbols of `alphabet`, shortest first. */
function sequences(alphabet: Op[], min: number, max: number): Op[][] {
let layer: Op[][] = [[]];
const all: Op[][] = min === 0 ? [[]] : [];
for (let length = 1; length <= max; length++) {
layer = layer.flatMap((sequence) => alphabet.map((op) => [...sequence, op]));
if (length >= min) all.push(...layer);
}
return all;
}

function field(declared: Declaration, ops: Op[]): FieldDecl {
return { declared, ops };
}

function singleCases(mode: Mode): Case[] {
const alphabet = fullAlphabet(DB_ONLY_KEY);
const cases: Case[] = [];
const add = (declarations: Declaration[], ops: Op[][]) => {
for (const declared of declarations) {
for (const sequence of ops) cases.push({ parent: { a: field(declared, sequence) }, children: [] });
}
};
const others = ALL_DECLARATIONS.filter((declared) => !MAIN_DECLARATIONS.includes(declared));
if (mode === "ci") {
add(MAIN_DECLARATIONS, sequences(alphabet, 0, 2));
add(others, sequences(alphabet, 0, 1));
add(MAIN_DECLARATIONS, sequences(coreAlphabet(DB_ONLY_KEY), 3, 3));
} else {
add(["property"], sequences(alphabet, 0, 3));
add(["accessor"], sequences(coreAlphabet(DB_ONLY_KEY), 3, 3));
add([...others, "accessor"], sequences(alphabet, 0, 2));
}
return cases;
}

function pairCases(mode: Mode): Case[] {
const first = sequences(coreAlphabet("b"), 0, 2);
const second = sequences(coreAlphabet("a"), 0, mode === "ci" ? 1 : 2);
const cases: Case[] = [];
for (const declaredA of ["property"] as const) {
for (const declaredB of ["defaulted", "setter"] as const) {
for (const opsA of first) {
for (const opsB of second) {
cases.push({ parent: { a: field(declaredA, opsA), b: field(declaredB, opsB) }, children: [] });
}
}
}
}
return cases;
}

function derivedCases(mode: Mode): Case[] {
const alphabet = coreAlphabet(DB_ONLY_KEY);
const parents = sequences(alphabet, 0, 1);
const firstChildren = sequences(alphabet, 0, mode === "ci" ? 1 : 2);
const siblings: Op[][] = [[], [access(ReadOnly)], [{ decorator: "Listable", required: true }]];
const cases: Case[] = [];
for (const declared of MAIN_DECLARATIONS) {
for (const opsP of parents) {
for (const opsA of firstChildren) {
for (const opsB of siblings) {
cases.push({
parent: { a: field(declared, opsP) },
children: [{ a: field(declared, opsA) }, { a: field(declared, opsB) }],
});
}
}
}
}
return cases;
}

/** The cases of a configuration within a mode's bounds, in a fixed order. */
export function casesOf(configuration: Configuration, mode: Mode): Case[] {
switch (configuration) {
case "single":
return singleCases(mode);
case "pair":
return pairCases(mode);
case "derived":
return derivedCases(mode);
}
}

/** The class names a case prints and builds under: `C` alone, else `P` and its children `A`, `B`, … */
export function classNames(testCase: Case): string[] {
if (testCase.children.length === 0) return ["C"];
return ["P", ...testCase.children.map((_, index) => String.fromCharCode(65 + index))];
}

function printMode(mode: AccessMode): string {
return `AccessMode.${AccessMode[mode]}`;
}

function printOverrides(overrides: Record<string, AccessMode>): string {
const entries = Object.entries(overrides).map(([action, mode]) => `${action}: ${printMode(mode)}`);
return `{ ${entries.join(", ")} }`;
}

/** A decorator as a developer writes it. */
export function printOp(op: Op): string {
switch (op.decorator) {
case "Access":
return op.overrides
? `@Access(${printMode(op.mode)}, ${printOverrides(op.overrides)})`
: `@Access(${printMode(op.mode)})`;
case "Computed":
return "@Computed(expr)";
case "Joined":
return `@Joined({ table, localKey: ${JSON.stringify(op.localKey)}, remoteField })`;
case "Listable": {
const args = [JSON.stringify(op.required), ...(op.mode ? [JSON.stringify(op.mode)] : [])];
return `@Listable(${op.required === true && !op.mode ? "" : args.join(", ")})`;
}
}
}

function printDeclaration(name: string, declared: Declaration): string {
switch (declared) {
case "property":
return `declare ${name}`;
case "defaulted":
return `${name} = "default:${name}"`;
case "getter":
return `get ${name}()`;
case "setter":
return `set ${name}(v)`;
case "accessor":
return `get ${name}() / set ${name}(v)`;
}
}

/** A field with its decorators top to bottom, as a developer writes them. */
function printField(name: string, decl: FieldDecl): string {
const decorators = [...decl.ops].reverse().map(printOp);
return [...decorators, printDeclaration(name, decl.declared)].join(" ");
}

function printClass(name: string, decl: ClassDecl, parent?: string): string {
const head = parent ? `${name} extends ${parent}` : name;
const fields = Object.entries(decl).map(([field, fieldDecl]) => printField(field, fieldDecl));
return `${head} { ${fields.join("; ")} }`;
}

/** A case as source, one class after the other in definition order. */
export function printCase(testCase: Case): string {
const [parent, ...children] = classNames(testCase);
return [
printClass(parent, testCase.parent),
...testCase.children.map((child, index) => printClass(children[index], child, parent)),
].join(" ");
}

/** How many decorators a case writes, over every class: the first measure of its size. */
export function opCount(testCase: Case): number {
return [testCase.parent, ...testCase.children]
.flatMap((decl) => Object.values(decl))
.reduce((count, decl) => count + decl.ops.length, 0);
}
Loading
Loading