docs: des contrats à deux voix, et une politique de version qui dit la vérité

Le régime des contrats est devenu bidirectionnel : chaque partie rédige sa
position dans son dépôt et tire celle de l'autre. Le fournisseur écrit son
engagement, chaque consommateur écrit sa déclaration — ce qu'il appelle
vraiment, sous quelles contraintes, et avec quelles frictions.

Ce dernier point change nos usages : la section Frictions est le chemin par
lequel un consommateur expose un problème, le canal hors-bande ne portant que le
signal. Les huit points que Festipod nous a remontés auraient dû arriver par là,
et notre réponse appartient à l'engagement plutôt qu'à un échange qui s'évapore.

Trois interfaces existaient, une seule était documentée. Elles ont maintenant
leur dossier et leurs deux voix : la surface du polyfill, ng-e2e-helpers, et la
couche d'indexation. On a écrit les engagements dont nous sommes l'auteur et les
déclarations des consommateurs qui sont à nous — délibérément AUCUN
usage_festipod : ce n'est pas notre dépôt, et décider à leur place ce qu'ils
consomment viderait le mécanisme de son sens.

Et la Change policy s'inverse. Elle disait « ce paquet n'offre pas de stabilité
sémantique ». Refuser un schéma ne ralentit pas le mouvement — ça retire au
consommateur le seul outil pour le gérer, alors qu'on faisait déjà le travail
d'une publication versionnée sans lui en donner l'étiquette.

Semver, donc, et les majeurs sont le cas NORMAL : une surface qui converge vers
une cible qu'elle n'atteint pas encore casse souvent, et c'est le signal vrai.
Le texte dit les déclencheurs plutôt que le nom du schéma — retirer un symbole
publié ou resserrer un appel est un majeur, ajouter est un mineur. Version
pleine sur main, pré-version sur une branche : Festipod peut épingler
aujourd'hui sans qu'on lui promette du stable, et sans qu'on ait à lui retirer
son adresse à la fusion.

Au passage, la signature publiée de watchShape était fausse — elle annonçait un
argument unique là où l'appel en prend deux. Rapporté par Festipod, confirmé
indépendamment.
This commit is contained in:
Sylvain Duchesne
2026-08-17 10:10:12 +02:00
parent a8d53010c2
commit 1957a8fe0c
8 changed files with 285 additions and 13 deletions
+2 -2
View File
@@ -29,10 +29,10 @@ This library exists so an application can be **written today against the NextGra
The distinguishing question, asked at every choice — published signature as much as internal mechanism: **does the target do this?** If it does it differently, we do it their way, whatever that costs. *Would a caller have to unlearn it?* comes second, and measures how bad a divergence is — never whether one is allowed. See `rule_no-divergence-from-nextgraph`.
This repo is the **provider** of `contract_polyfill-surface`; consuming applications live in other repos and pull it. The per-symbol ruling, with an epistemic label on every target-side claim, stays here in `docs/api-contract.md` — that is maintainer material, not the engagement.
This repo is the **provider** of `polyfill-surface`; consuming applications live in other repos, pull the engagement, and declare back what they actually consume — one `usage_<consumer>.md` beside the contract, in the interface folder. The per-symbol ruling, with an epistemic label on every target-side claim, stays here in `docs/api-contract.md` — that is maintainer material, not the engagement.
## Read first
- `contract_polyfill-surface` — the engagement itself, written from the caller's point of view.
- `polyfill-surface/contract_polyfill-surface` — the engagement itself, written from the caller's point of view.
- `rule_no-divergence-from-nextgraph` — the test that decides what may be built at all.
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
@@ -55,7 +55,17 @@ export const storeRegistry: { // no identity parameter — a session is
export async function readUnion(docs: NuriLike[]): Promise<UnionSubject[]>;
export interface UnionSubject { subject: string; graph: Nuri; props: Record<string, string[]> }
export function useShape(shapeType: unknown, scope: unknown): unknown; // read-filtered view
export function watchShape(query: ShapeQuery): ShapeObservable;
// TWO positional arguments — the same pair `useShape` takes. `ShapeQuery` is what you
// READ BACK (the snapshot), never what you pass in.
export function watchShape<T = UnionSubject>(shapeType: unknown, scope: Scope): ShapeObservable<T>;
export interface ShapeObservable<T = UnionSubject> {
getSnapshot(): ShapeQuery<T>; // stable reference until the state changes
subscribe(onChange: () => void): () => void; // returns its own unsubscribe
refetch(): void; // re-resolve and re-read now; never polls
}
export interface ShapeQuery<T = UnionSubject> {
data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown;
}
export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
@@ -153,6 +163,16 @@ The same rule reaches what a call hands BACK, not only what it looked up first:
## Change policy
This surface changes, and shrinks. The package does not offer semantic-version stability.
**Semver, and majors are the normal case.** This surface converges on a NextGraph that does not ship yet, so most steps toward the target remove or narrow something — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
Re-pull this contract at every upgrade.
What each level means here, in this package's own terms:
- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession` and `inbox.materialize` all left this way), **or** an existing call narrows: it now throws where it returned (`listMyEntityDocs` refuses a listing whose documents you could not open; `ensureIdentity` rejects rather than resolve on an incomplete restore), or it reports a state you did not have to handle before (`watchShape` publishing `isError` where it used to publish a synced-empty snapshot). A signature change a caller must react to counts; one that only accepts more than before does not.
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.1`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.1`, the string you pinned, never the version it is heading for.
There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it, and a symbol is never left published-but-dead as a courtesy. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first.
+6 -1
View File
@@ -2,9 +2,10 @@
type: overview
summary: What each end-to-end suite judges, the shortcut that kept two defects invisible, and the failure mode that is not ours to fix
triggers:
keywords: [e2e, harness, playwright, chromium, journey, suite, profile, broker page, notebook, wallet profile]
keywords: [e2e, harness, playwright, chromium, journey, suite, profile, broker page, notebook, wallet profile, ng-e2e-helpers]
paths:
- "packages/polyfill/e2e/**"
- "packages/ng-e2e-helpers/**"
vocabulary:
- term: journey
gloss: one named end-to-end story in the applicative suite, reported pass or fail as a whole
@@ -20,8 +21,12 @@ Three suites drive real browsers against the real broker and the real wallet app
They are also expensive and, on a machine whose network moves, unreliable in ways that look exactly like product defects. Most of what this concept holds exists so an agent does not spend hours diagnosing a product bug that is a transport failure — or, worse, trust a green run that measured nothing.
The generic half of that machinery is a package of its own, `ng-e2e-helpers`, and this repo **publishes** it: a NextGraph application in any repository pulls `ng-e2e-helpers/contract_ng-e2e-helpers` and declares back what it consumes. Our own suites are one of those consumers, and declare it in the same folder — same pair, no boundary to cross.
## Read first
- `ng-e2e-helpers/contract_ng-e2e-helpers` — the engagement, written from the caller's point of view.
- `ng-e2e-helpers/usage_polyfill-e2e` — what our own suites actually call, and where the fit still costs them code.
- `knowledge_what-each-suite-judges` — which suite answers which question.
- `rule_never-shortcut-the-sign-in` — the shortcut that hid a 404 and a regression at once.
- `caveat_a-dropped-pipe-kills-a-run` — the failure mode that is not ours.
@@ -0,0 +1,161 @@
---
type: contract
summary: The machinery ng-e2e-helpers offers a NextGraph application's end-to-end suite — wallets, the broker crossing, per-run profiles, bounds, and a bounded report
---
# contract_ng-e2e-helpers — `ng-e2e-helpers`
## Scope
This package is the end-to-end testing machinery a NextGraph application needs to get a real person into itself: minting a wallet by driving the wallet application, crossing the broker, and coming back inside the iframe the application runs in — plus per-run browser profiles, bounds that turn a hang into a named failure, and a run report whose size does not depend on what failed.
It covers **only what is generic to NextGraph**. It knows nothing about any one application, and nothing about any compatibility layer: an application calling the NextGraph SDK directly is its intended consumer. What is specific to your repository — the page that carries your application, how you build your bundle, which journeys you run — is yours to write, and this package deliberately offers no place to put it.
It is not a test runner, not an assertion library, and not a fixture system. You keep your own runner and your own `main()`.
### Deployment requirements
`playwright` and `@ng-org/web` are **peer dependencies**, and you own both versions: browser binaries have to match the driver, and the SDK the export page opens a session with must be the one your application and your broker agree on. Import the browser types from the helpers that return them rather than from `playwright` directly — a second resolution of the driver produces a structurally different `BrowserContext`, and a context you opened then cannot be handed back to the helper that opens contexts.
A machine running this needs a real Chromium, network reach to the wallet application and to the broker, and a writable temporary directory for the per-run profiles.
## Surface
Full typed shape: the package's `types` entry, `ng-e2e-helpers`. It also ships one executable, `ng-mint-wallet`, for provisioning a deployment's wallet file outside any run. The load-bearing signatures:
```ts
// ── bounds: a hang becomes a named failure ───────────────────────────────────
export function within<T>(what: string, ms: number, task: () => Promise<T>): Promise<T>;
export class DeadlineExceeded extends Error {} // thrown by `within` — names `what`
export class BrowserGone extends Error {} // the browser died; not the task's fault
export function enclosingBound(steps: readonly number[], margin: number): number;
export function closeQuietly(what: string, close: () => Promise<unknown>): Promise<void>;
export function armSuiteDeadline(suite: string, ms: number, thenReport?: () => void): void;
export function browserLost(reason: string): void; // declare it, once
export function lossDeclared(): string | null; // has it been declared?
export function firstLine(e: unknown): string;
export const CLOSE_MS: number, CONTEXT_ACTION_MS: number, CONTEXT_NAVIGATION_MS: number;
// ── measurement: how a bound gets its number ─────────────────────────────────
export function measured<T>(what: string, bound: number, task: (ms: number) => Promise<T>): Promise<T>;
export function record(what: string, ms: number, ok: boolean, bound: number): void;
export function timingsWanted(): boolean; // true under E2E_TIMINGS=1
export function printTimings(): void;
// ── browser and profiles: one run owns its own ───────────────────────────────
export interface RunProfile { readonly dir: string; readonly purpose: string; discard(): void }
export function newRunProfile(purpose: string): RunProfile;
export function isAlive(pid: number): boolean;
export function launchWatchedContext(label: string, dir: string): Promise<BrowserContext>;
export function closeContext(label: string, ctx: BrowserContext): Promise<void>;
export function newPage(label: string, ctx: BrowserContext): Promise<Page>;
export const LAUNCH_MS: number, NEW_PAGE_MS: number;
// ── the wallet: minted, carried, imported ────────────────────────────────────
export interface WalletCredentials { readonly name: string; readonly password: string }
export const DEFAULT_WALLET_NAME: string; // the password never has a default
export function mintWalletProfile(purpose: string, c: WalletCredentials): Promise<RunProfile>;
export function mintWalletProfileKeepingContext(
purpose: string, c: WalletCredentials): Promise<{ ctx: BrowserContext; profile: RunProfile }>;
export function mintWalletBytes(password: string, name?: string): Promise<Uint8Array>;
export function createWalletInContext(ctx: BrowserContext, c: WalletCredentials): Promise<void>;
export function emptyProfileContext(
purpose: string): Promise<{ ctx: BrowserContext; profile: RunProfile }>;
export function exportWalletBytes(ctx: BrowserContext, walletPassword: string): Promise<Uint8Array>;
export function exportWalletFile(ctx: BrowserContext, ngwPath: string, pw: string): Promise<number>;
export function importWalletFile(page: Page, ngwPath: string, password: string): Promise<void>;
// ── the broker crossing ──────────────────────────────────────────────────────
export function setupBrokerPage(page: Page, appUrl: string, walletPassword: string): Promise<Frame>;
export function completeBrokerLogin(page: Page, appUrl: string, pw: string): Promise<Frame>;
export const BROKER_LOGIN_MS: number, BROKER_ROUND_TRIP_MS: number;
export function brokerRedirectFor(appUrl: string): string;
export const BROKER_SCREENS: readonly BrokerScreenSpec[]; // the screens, in test order
export const WALLET_APP, WALLET_CREATION, WALLET_IMPORT; // the wallet application's own pages
export type BrokerScreen = "choose-broker" | "login-offered" | "wallet-list"
| "password" | "working" | "error";
export type { BrokerScreenSpec, ScreenSignature, ScreenResponse, TextPattern };
// ── serving your application to the browser ──────────────────────────────────
export function serveOnEphemeralPort(
handler: (req: IncomingMessage, res: ServerResponse) => void,
): Promise<{ url: string; close: () => void }>;
// ── failures that are not the application's ──────────────────────────────────
export function browserTrouble(label: string, ctx: BrowserContext): Promise<string | null>;
export function frameTrouble(id: string, page: Page, frame: Frame, marker: string): Promise<string | null>;
export const BROWSER_PROBE_MS: number, FRAME_PROBE_MS: number;
// ── the report ───────────────────────────────────────────────────────────────
export interface JourneyDeclaration { readonly name: string; readonly checks: readonly string[] }
export type Prerequisite = () => Promise<string | null> | (string | null);
export interface JourneySpec {
readonly name: string; // must be a declared journey
readonly needs?: readonly Prerequisite[]; // each answers null, or why it cannot start
readonly run: () => Promise<void>;
}
export interface SuiteOptions {
readonly label: string;
readonly journeys: readonly JourneyDeclaration[];
readonly journeyBound: number;
readonly diagnose?: () => Promise<string | null>;
}
export interface SuiteReport {
check(name: string, ok: boolean, detail?: string): void; // throws on an undeclared name
journey(spec: JourneySpec): Promise<void>;
finish(fatal: string | null): never; // prints, then leaves
}
export function declareSuite(options: SuiteOptions): SuiteReport;
```
## Guarantees
**A bound names what it was waiting for.** `within(what, ms, task)` either settles the task or throws a `DeadlineExceeded` carrying `what` verbatim — never a bare timeout with no subject. `armSuiteDeadline` does the same for a whole run, and reports before leaving. `enclosingBound(steps, margin)` returns a number no smaller than the sum of the bounds it encloses, so an enclosure can never fire before the step that actually hung.
**A run owns its profile and discards it.** `newRunProfile` returns a directory belonging to this run alone; `discard()` kills whatever still holds it and removes it, is idempotent, and also runs when the process leaves — including when the run is killed. Two runs never share local state, so nothing a previous run left can make this one pass or fail.
**A minted wallet is new.** `mintWalletProfile`/`mintWalletBytes` walk the wallet application to create a wallet that did not exist a moment ago. `mintWalletBytes` keeps only the bytes and discards the profile it minted in; `mintWalletProfile` keeps the profile for the length of the run. The password is always a parameter and never has a default; the name defaults to `DEFAULT_WALLET_NAME` because nothing in NextGraph keys off it.
**The crossing dispatches on the screen it can see**, not on a fixed sequence: `setupBrokerPage` reads the page against `BROKER_SCREENS` in order, answers it, and returns the application's `Frame` once the crossing has completed. It identifies the application by ORIGIN. A terminal screen ends the crossing as a failure rather than an action.
**An empty-profile context starts with nothing local.** `emptyProfileContext` gives a context whose profile holds no wallet and no repo cache, which is the reconnection cold start: the wallet's repos are on the broker and not in this profile. `importWalletFile` then puts the wallet in — but not the repos' cache — so the next session over it still hits that cold start.
**A closed server is closed.** `serveOnEphemeralPort` tracks its open sockets and destroys them on `close()`, so a connection nobody hung up cannot be blamed on whatever goes wrong next.
**A known failure mode is named instead of the innocent operation.** `browserTrouble`/`frameTrouble` answer a string when the browser or the application frame is the actual cause, and `null` when they are not. `SuiteOptions.diagnose` puts that answer in FRONT of a journey's own reason, never in place of it. A frame that is attached, on the right URL, and empty is reported as trouble — that is the state a naive check misses.
**The report's size does not depend on what failed.** Journeys and their checks are declared up front to `declareSuite`; `check` throws on a name the journey did not declare, `journey` runs one journey bounded and isolated so it cannot change the report's shape, and `finish` reports every declared check the run did not get to before printing and exiting. A journey whose prerequisite is provably dead is reported as such instead of being driven.
**Errors are values where a caller can act on them.** `browserTrouble`/`frameTrouble`/`Prerequisite` answer `string | null`, never throw for the condition they detect. `DeadlineExceeded` and `BrowserGone` are exported classes, so `instanceof` is a supported way to tell a hang from a dead browser.
## Non-guarantees
**No assertion library, no runner, no fixtures.** `SuiteReport.check` records a boolean you computed; nothing here decides what is true.
**`finish` never returns** — it exits the process. Do not put cleanup after it; put it in the `discard()`/`closeQuietly` path.
**No message text is stable.** The strings from `browserTrouble`, `frameTrouble`, `DeadlineExceeded` and the report are for a human reading a run. Do not parse them, match on them, or branch on them.
**No timing promise.** Every exported `*_MS` constant is a bound sized from a measurement on one machine, not a service level: they change when the measurements change, and a run slower than one of them is a failed run, not a broken guarantee. `E2E_TIMINGS=1` reprints the measurements so you can resize your own.
**Nothing survives a run.** Profiles are discarded, and no artifact, cache, or wallet is carried from one run to the next. The one exception is a file you write yourself with `exportWalletFile` or `ng-mint-wallet`.
**No concurrency contract.** The suite machinery assumes one run per process. Two suites sharing a process share `armSuiteDeadline`, the timings table and the loss declaration.
**No wallet-application version pinning.** The crossing is driven against the wallet application as deployed at the broker you point it at. When that application changes its screens, `BROKER_SCREENS` is what has to be updated — a crossing that fails there is not necessarily your application's fault, and is not this package's promise to hold across upstream redesigns.
## Change policy
**Semver, and majors are the normal case.** This machinery tracks a NextGraph and a wallet application that are both still moving, so a screen inventory, a bound, or a call shape changes whenever they do — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
What each level means here, in this package's own terms:
- **major** — an exported symbol is removed or renamed, **or** an existing call narrows: it now throws where it returned, requires an argument that was optional, or returns something a caller must newly handle. A change to what a `SuiteReport` accepts (a check name, a journey shape) is a major, because your suite is written against it. A signature change a caller must react to counts; one that only accepts more than before does not.
- **minor** — a symbol is added and nothing existing moves: a new helper, a new screen in `BROKER_SCREENS`, a new optional field on `SuiteOptions`.
- **patch** — a fix that changes neither the exported surface nor anything above under `## Guarantees` — including a re-sized `*_MS` constant, which is explicitly disclaimed above.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it. What exists today is `1.0.0-dev.1`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: ng-e2e-helpers@1.0.0-dev.1`, the string you pinned, never the version it is heading for.
There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first.
@@ -0,0 +1,55 @@
---
type: usage
summary: What the polyfill's own end-to-end suites call from ng-e2e-helpers, under which constraints, and where the fit still costs them code
against: ng-e2e-helpers@1.0.0-dev.1
---
# usage_polyfill-e2e — the polyfill's end-to-end suites
The consumer is the end-to-end suite of `@ng-eventually/polyfill`: four entry points under that package's `e2e/` — a batch of SDK checks against the broker, an applicative suite driving the example application, a reactivity probe, and a cold-start reproduction — plus the page module they share.
This consumer lives in the same repository as the provider. That changes nothing about what is declared here: the engagement is what these suites are written against, and anything they call that is NOT listed below is theirs to fix if it moves.
## Consumed surface
**Bounds**`within`, `armSuiteDeadline`, `closeQuietly`, `enclosingBound`, `firstLine`. `within` is the most-called entry of the whole package here: every bridge call into the application iframe goes through it.
**Measurement**`measured`. Nothing reads `record`, `timingsWanted` or `printTimings` directly; the `E2E_TIMINGS=1` output is consumed as printed output, not as a call.
**Browser and profiles**`launchWatchedContext`, `closeContext`, `newPage`, and the type `RunProfile`. `newRunProfile` and `isAlive` are never called: profiles are always obtained through a wallet call.
**Wallet**`mintWalletProfile` (the batch wallet, minted once per run), `mintWalletProfileKeepingContext` (the cold-start reproduction, which needs the very first application session over a never-used wallet), `emptyProfileContext` + `importWalletFile` (the reconnection journey), `exportWalletBytes` (serving the wallet from the application's own bundle), and the type `WalletCredentials`.
**Broker crossing**`setupBrokerPage` in all four suites, `completeBrokerLogin` in the applicative one.
**Serving**`serveOnEphemeralPort`, for both the SDK page and the example application's bundle.
**Known failure modes**`browserTrouble`, `frameTrouble`.
**Report**`declareSuite` and the types `JourneyDeclaration`, `Prerequisite`. Used by the applicative suite only.
**Constants**`BROKER_ROUND_TRIP_MS`, `NEW_PAGE_MS`, both as inputs to `enclosingBound` rather than as waits of their own.
Everything else the package exports is offered and NOT consumed here — the screen inventory (`BROKER_SCREENS`, `WALLET_APP`, `WALLET_CREATION`, `WALLET_IMPORT`, `brokerRedirectFor`, and the screen types), `createWalletInContext`, `exportWalletFile`, `mintWalletBytes`, `DEFAULT_WALLET_NAME`, `browserLost`, `lossDeclared`, the exported error classes, and the remaining `*_MS` constants. It is safely evolvable as far as these suites are concerned.
## Constraints
**One Playwright, and the browser types come from the helpers.** These suites import `Frame`, `Page` and `BrowserContext` from `playwright` directly today, which holds only because the package resolves the same copy. The safer form — deriving them from the helpers that return them — is what a consumer in another repository must do, and what this one should adopt.
**Every suite mints its own physical wallet, per run.** Nothing is carried between runs and no wallet is reused, so a run's result never depends on a previous one. The batch wallet's NAME is stable and its identity is not: two runs sharing the name share nothing else.
**The password is supplied, never defaulted.** These suites also read the barrier's displayed password back off its own screen and pass it to `importWalletFile`, which is the only way to prove that what the barrier shows is what opens the file. A defaulted password would make that check vacuous.
**An enclosing bound is at least the sum of what it encloses.** These suites compute every enclosure with `enclosingBound` rather than picking a round number, because an enclosure that fires first reports "the journey timed out" and never names the step that hung.
**A wait must not be entered unbounded.** Playwright's `frame.evaluate` waits forever, so no bridge call is made outside `within`.
## Frictions
**`frame.evaluate` has no bound, and three suites each wrote the same wrapper.** A `sdk(frame, method, …args)` helper — `within(…, BRIDGE_MS, () => frame.evaluate(…))` — is duplicated verbatim in three of the four entry points. The bound is the single most important one in each of them, and the package offers nothing to obtain it from. A bounded `evaluate` helper here would delete the duplication and stop each new suite re-deriving it.
**"Measured and bounded" is one intent and two calls.** Sizing a bound from its own measurement is the discipline this package prescribes, yet the applicative suite composes `measured(what, ms, (bound) => within(what, bound, task))` by hand — and the indexing layer's suite, in another repository, wrote the same line byte for byte. A helper that pairs them belongs on the engagement.
**`declareSuite` fits a journey suite and not a check batch.** Only the applicative suite uses it; the other three declare a local `record`/`check` pair and print their own summary, because their shape is a flat list of checks discovered as the run goes rather than named journeys declared up front. The up-front declaration is exactly what makes the report bounded, so this is not a request to relax it — but three of four suites falling outside it suggests the package covers one of two real shapes.
**The screen inventory is exported and unreachable in practice.** `BROKER_SCREENS` and the `WALLET_*` page descriptions are published, but a suite that crosses the broker with `setupBrokerPage` never needs them, and one that wanted to extend the crossing has no supported way to hand a modified inventory back in. As it stands they read as internals that happen to be exported.
+36 -5
View File
@@ -1,11 +1,42 @@
# Inter-repo contracts. `publish:` is this project's engagement toward its consumers —
# listing a leaf here IS the act of publishing it; an unlisted `contract_` leaf is a draft.
#
# This project is a PROVIDER only for now: the applications that consume the polyfill live in
# their own repos and pull `polyfill-surface` from here. Nothing is consumed in the other
# direction — the NextGraph clone this library aligns on is a read-only reference we
# verify against, not a contract anyone stamps.
# This project is a PROVIDER of two interfaces, each hosted in an interface folder inside
# its owning concept, with one `usage_<consumer>.md` per consumer beside the engagement:
#
# polyfill-surface (concepts/app-contract/polyfill-surface/)
# consumers: the Festipod application (its own repo), `ng-helpers`
# ng-e2e-helpers (concepts/e2e-harness/ng-e2e-helpers/)
# consumers: this repo's own polyfill e2e suites (same repo — declared in place,
# nothing to pull), `ng-helpers`, any NextGraph application
#
# `consume:` here is the PROVIDER side of the pair: we pull each consumer's own declaration
# back, so we can see what is actually relied on and read its Frictions. We consume no
# engagement — the NextGraph clone this library aligns on is a read-only reference we verify
# against, not a contract anyone stamps.
#
# `pullFrom:` values are CANONICAL remote identities, because this file travels with the
# branch. Per-developer local access lives in `.project/contracts.local.yaml`, which is
# gitignored and must never be committed.
publish:
# paths are relative to `.project/`
polyfill-surface: concepts/app-contract/contract_polyfill-surface.md
polyfill-surface: concepts/app-contract/polyfill-surface/contract_polyfill-surface.md
ng-e2e-helpers: concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md
consume:
# Festipod is NOT listed: it has not declared a usage leaf, and we do not author one for
# a consumer whose repo is not ours. An interface with no declared consumer degrades to
# the one-document mode, which is what `polyfill-surface` is for Festipod today.
- contract: ng-helpers-uses-polyfill-surface
type: git
pullFrom: git@gitea.reconnexion.apps.gueraud.net:Sylvain/ng-helpers.git/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md
ref: main
into: concepts/app-contract/polyfill-surface/
- contract: ng-helpers-uses-ng-e2e-helpers
type: git
pullFrom: git@gitea.reconnexion.apps.gueraud.net:Sylvain/ng-helpers.git/.project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md
ref: main
into: concepts/e2e-harness/ng-e2e-helpers/