@fairfox/polly 0.77.3 → 0.78.0
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/src/elysia/index.js.map +2 -2
- package/dist/src/mesh.js +60 -38
- package/dist/src/mesh.js.map +5 -5
- package/dist/src/peer.js +2 -2
- package/dist/src/peer.js.map +4 -4
- package/dist/src/shared/lib/mesh-client.d.ts +38 -0
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -5
- package/dist/src/shared/lib/mesh-state.d.ts +21 -0
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +5 -0
- package/dist/src/shared/lib/peer-repo-server.d.ts +15 -0
- package/dist/tools/verify/src/cli.js +161 -1
- package/dist/tools/verify/src/cli.js.map +11 -9
- package/dist/tools/verify/src/config.d.ts +10 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/package.json +1 -1
|
@@ -53,6 +53,11 @@ export interface CreatePeerStateClientOptions {
|
|
|
53
53
|
sign?: boolean;
|
|
54
54
|
/** Keyring for the signing layer. Required when `sign` is true. */
|
|
55
55
|
keyring?: MeshKeyring;
|
|
56
|
+
/** Network-adapter factory. Defaults to constructing a real
|
|
57
|
+
* `WebSocketClientAdapter` against `url`. Injecting a factory lets tests (or
|
|
58
|
+
* instrumentation) substitute a fake adapter so they stay off the network —
|
|
59
|
+
* the production path leaves this unset. */
|
|
60
|
+
adapterFactory?: (url: string, retryInterval?: number) => WebSocketClientAdapter;
|
|
56
61
|
}
|
|
57
62
|
export interface PeerStateClient {
|
|
58
63
|
/** A configured Repo backed by the WebSocket relay. Pass to
|
|
@@ -88,6 +88,21 @@ export interface PeerRepoServer {
|
|
|
88
88
|
dryRun?: boolean;
|
|
89
89
|
}) => Promise<SweepResult>;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Recover every documentId from a NodeFS storage tree.
|
|
93
|
+
*
|
|
94
|
+
* automerge-repo's NodeFS adapter shards each document two levels deep —
|
|
95
|
+
* the first two characters of the documentId, then the remainder — so
|
|
96
|
+
* the directory structure is itself the document list. A real document
|
|
97
|
+
* is a directory (holding `snapshot` / `incremental` chunk
|
|
98
|
+
* subdirectories); flat files such as the storage-adapter-id are
|
|
99
|
+
* skipped. A missing storage directory (a server that has never
|
|
100
|
+
* written) yields an empty list.
|
|
101
|
+
*
|
|
102
|
+
* Exported for unit testing only — not re-exported from the package's
|
|
103
|
+
* public entry points.
|
|
104
|
+
*/
|
|
105
|
+
export declare function listNodeFSDocumentIds(baseDirectory: string): Promise<string[]>;
|
|
91
106
|
/**
|
|
92
107
|
* Construct a Polly peer-relay server. Returns a Repo that participates as
|
|
93
108
|
* the always-on peer, the underlying WebSocket server and storage adapter
|
|
@@ -249,6 +249,50 @@ var init_expression_validator = __esm(() => {
|
|
|
249
249
|
WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
+
// tools/verify/src/analysis/coupled-fields.ts
|
|
253
|
+
var exports_coupled_fields = {};
|
|
254
|
+
__export(exports_coupled_fields, {
|
|
255
|
+
checkCoupledFields: () => checkCoupledFields
|
|
256
|
+
});
|
|
257
|
+
function norm(field) {
|
|
258
|
+
return field.replace(/_/g, ".");
|
|
259
|
+
}
|
|
260
|
+
function checkCoupledFields(coupledFields, handlers) {
|
|
261
|
+
const violations = [];
|
|
262
|
+
for (const rawGroup of coupledFields) {
|
|
263
|
+
const group = rawGroup.map(norm);
|
|
264
|
+
const groupSet = new Set(group);
|
|
265
|
+
if (groupSet.size < 2)
|
|
266
|
+
continue;
|
|
267
|
+
for (const handler of handlers) {
|
|
268
|
+
const violation = subsetViolation(group, groupSet, handler);
|
|
269
|
+
if (violation)
|
|
270
|
+
violations.push(violation);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { valid: violations.length === 0, violations };
|
|
274
|
+
}
|
|
275
|
+
function writtenFields(group, handler) {
|
|
276
|
+
const written = new Set;
|
|
277
|
+
for (const assignment of handler.assignments) {
|
|
278
|
+
const field = norm(assignment.field);
|
|
279
|
+
if (group.has(field))
|
|
280
|
+
written.add(field);
|
|
281
|
+
}
|
|
282
|
+
return written;
|
|
283
|
+
}
|
|
284
|
+
function subsetViolation(group, groupSet, handler) {
|
|
285
|
+
const written = writtenFields(groupSet, handler);
|
|
286
|
+
if (written.size === 0 || written.size === groupSet.size)
|
|
287
|
+
return null;
|
|
288
|
+
return {
|
|
289
|
+
group,
|
|
290
|
+
handler: handler.messageType,
|
|
291
|
+
written: group.filter((f) => written.has(f)),
|
|
292
|
+
missing: group.filter((f) => !written.has(f))
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
252
296
|
// tools/verify/src/analysis/non-interference.ts
|
|
253
297
|
var exports_non_interference = {};
|
|
254
298
|
__export(exports_non_interference, {
|
|
@@ -2678,6 +2722,9 @@ var init_tla = __esm(() => {
|
|
|
2678
2722
|
this.indent--;
|
|
2679
2723
|
this.indent--;
|
|
2680
2724
|
this.line("");
|
|
2725
|
+
if (config.capabilities && config.capabilities.length > 0) {
|
|
2726
|
+
this.addCapabilityInvariants(config.capabilities);
|
|
2727
|
+
}
|
|
2681
2728
|
if (this.extractedInvariants.length > 0) {
|
|
2682
2729
|
this.line("\\* Extracted invariants from code annotations");
|
|
2683
2730
|
this.line("");
|
|
@@ -2694,6 +2741,15 @@ var init_tla = __esm(() => {
|
|
|
2694
2741
|
this.line("\\* State constraint to bound state space");
|
|
2695
2742
|
this.addStateConstraint(config, _analysis);
|
|
2696
2743
|
}
|
|
2744
|
+
addCapabilityInvariants(capabilities) {
|
|
2745
|
+
for (const cap of capabilities) {
|
|
2746
|
+
this.extractedInvariants.push({
|
|
2747
|
+
name: `Capability_${cap.name}`,
|
|
2748
|
+
description: cap.message ? `polly#160 capability: ${cap.message}` : `polly#160 capability: ${cap.name} requires its precondition`,
|
|
2749
|
+
expression: `(!(${cap.enabledBy})) || (${cap.requires})`
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2697
2753
|
addTemporalConstraints(constraints) {
|
|
2698
2754
|
this.line("\\* Tier 2: Temporal constraint invariants");
|
|
2699
2755
|
this.line("\\* Enforce ordering requirements between message types");
|
|
@@ -3910,6 +3966,85 @@ function generateConfig(analysis, projectType = "chrome-extension") {
|
|
|
3910
3966
|
// tools/verify/src/config/parser.ts
|
|
3911
3967
|
import * as fs from "node:fs";
|
|
3912
3968
|
import * as path from "node:path";
|
|
3969
|
+
|
|
3970
|
+
// tools/verify/src/config/capability-validation.ts
|
|
3971
|
+
init_expression_validator();
|
|
3972
|
+
function validateCapabilities(capabilities, stateConfig) {
|
|
3973
|
+
if (!capabilities || capabilities.length === 0)
|
|
3974
|
+
return [];
|
|
3975
|
+
const issues = [];
|
|
3976
|
+
const configKeys = new Set(Object.keys(stateConfig));
|
|
3977
|
+
for (const cap of capabilities) {
|
|
3978
|
+
const name = cap.name?.trim();
|
|
3979
|
+
if (!name) {
|
|
3980
|
+
issues.push({
|
|
3981
|
+
type: "invalid_value",
|
|
3982
|
+
severity: "error",
|
|
3983
|
+
field: "capabilities",
|
|
3984
|
+
message: "Capability is missing a name.",
|
|
3985
|
+
suggestion: "Give each capability a unique name; it becomes the TLA+ invariant identifier."
|
|
3986
|
+
});
|
|
3987
|
+
continue;
|
|
3988
|
+
}
|
|
3989
|
+
issues.push(...validateExpression(name, "enabledBy", cap.enabledBy, configKeys, stateConfig), ...validateExpression(name, "requires", cap.requires, configKeys, stateConfig));
|
|
3990
|
+
}
|
|
3991
|
+
return issues;
|
|
3992
|
+
}
|
|
3993
|
+
function validateExpression(name, slot, expr, configKeys, stateConfig) {
|
|
3994
|
+
const field = `capabilities.${name}.${slot}`;
|
|
3995
|
+
if (!expr?.trim()) {
|
|
3996
|
+
return [
|
|
3997
|
+
{
|
|
3998
|
+
type: "capability_empty_expression",
|
|
3999
|
+
severity: "error",
|
|
4000
|
+
field,
|
|
4001
|
+
message: `Capability "${name}" has an empty ${slot} expression.`,
|
|
4002
|
+
suggestion: `Provide a state expression for ${slot}, e.g. "state.authenticated".`
|
|
4003
|
+
}
|
|
4004
|
+
];
|
|
4005
|
+
}
|
|
4006
|
+
const refs = extractFieldRefs(expr);
|
|
4007
|
+
if (refs.length === 0) {
|
|
4008
|
+
return [
|
|
4009
|
+
{
|
|
4010
|
+
type: "capability_empty_expression",
|
|
4011
|
+
severity: "error",
|
|
4012
|
+
field,
|
|
4013
|
+
message: `Capability "${name}" ${slot} expression "${expr}" references no state field.`,
|
|
4014
|
+
suggestion: 'Reference state via the state./.value form (e.g. "state.authReady"); a bare identifier produces a silently-vacuous invariant.'
|
|
4015
|
+
}
|
|
4016
|
+
];
|
|
4017
|
+
}
|
|
4018
|
+
return refs.filter((ref) => !fieldInConfig(ref, configKeys, stateConfig)).map((ref) => ({
|
|
4019
|
+
type: "capability_unknown_field",
|
|
4020
|
+
severity: "error",
|
|
4021
|
+
field,
|
|
4022
|
+
message: `Capability "${name}" ${slot} references "${ref}", which is not in the state config.`,
|
|
4023
|
+
suggestion: `Add "${ref}" to state, or correct the expression. Known fields: ${[...configKeys].join(", ")}`
|
|
4024
|
+
}));
|
|
4025
|
+
}
|
|
4026
|
+
function validateCoupledFields(coupledFields, stateConfig) {
|
|
4027
|
+
if (!coupledFields || coupledFields.length === 0)
|
|
4028
|
+
return [];
|
|
4029
|
+
const issues = [];
|
|
4030
|
+
const configKeys = new Set(Object.keys(stateConfig));
|
|
4031
|
+
coupledFields.forEach((group, i) => {
|
|
4032
|
+
for (const field of group) {
|
|
4033
|
+
if (!fieldInConfig(field, configKeys, stateConfig)) {
|
|
4034
|
+
issues.push({
|
|
4035
|
+
type: "coupled_unknown_field",
|
|
4036
|
+
severity: "error",
|
|
4037
|
+
field: `coupledFields[${i}]`,
|
|
4038
|
+
message: `Coupled field "${field}" is not in the state config.`,
|
|
4039
|
+
suggestion: `Use a declared state field. Known fields: ${[...configKeys].join(", ")}`
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
});
|
|
4044
|
+
return issues;
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// tools/verify/src/config/parser.ts
|
|
3913
4048
|
class ConfigValidator {
|
|
3914
4049
|
issues = [];
|
|
3915
4050
|
validate(configPath) {
|
|
@@ -4032,6 +4167,8 @@ class ConfigValidator {
|
|
|
4032
4167
|
if (config.subsystems) {
|
|
4033
4168
|
this.validateSubsystems(config.subsystems, config.state, config.messages);
|
|
4034
4169
|
}
|
|
4170
|
+
this.issues.push(...validateCapabilities(config.capabilities, config.state));
|
|
4171
|
+
this.issues.push(...validateCoupledFields(config.coupledFields, config.state));
|
|
4035
4172
|
}
|
|
4036
4173
|
findNullPlaceholders(obj, path2) {
|
|
4037
4174
|
if (obj === null || obj === undefined) {
|
|
@@ -7941,6 +8078,28 @@ function getMaxDepth(config) {
|
|
|
7941
8078
|
}
|
|
7942
8079
|
return;
|
|
7943
8080
|
}
|
|
8081
|
+
async function runCoupledFieldsLint(config, analysis) {
|
|
8082
|
+
const groups = config.coupledFields ?? [];
|
|
8083
|
+
if (groups.length === 0)
|
|
8084
|
+
return;
|
|
8085
|
+
const { checkCoupledFields: checkCoupledFields2 } = await Promise.resolve().then(() => exports_coupled_fields);
|
|
8086
|
+
const result = checkCoupledFields2(groups, analysis.handlers);
|
|
8087
|
+
if (result.valid) {
|
|
8088
|
+
console.log(color("✓ Coupled fields: verified (no partial-subset writes)", COLORS.green));
|
|
8089
|
+
console.log();
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
console.log(color(`⚠️ Coupled-field violations detected:
|
|
8093
|
+
`, COLORS.yellow));
|
|
8094
|
+
for (const v of result.violations) {
|
|
8095
|
+
console.log(color(` • Handler "${v.handler}" writes {${v.written.join(", ")}} but not {${v.missing.join(", ")}} of coupled group {${v.group.join(", ")}}`, COLORS.yellow));
|
|
8096
|
+
}
|
|
8097
|
+
console.log();
|
|
8098
|
+
console.log(color(" These fields are declared to move together; a capability granted without its", COLORS.yellow));
|
|
8099
|
+
console.log(color(" precondition is a likely cause. Co-write the full group in each handler, or model", COLORS.yellow));
|
|
8100
|
+
console.log(color(" the relationship with a `capabilities` invariant.", COLORS.yellow));
|
|
8101
|
+
console.log();
|
|
8102
|
+
}
|
|
7944
8103
|
async function runFullVerification(configPath) {
|
|
7945
8104
|
const config = await loadVerificationConfig(configPath);
|
|
7946
8105
|
const analysis = await runCodebaseAnalysis();
|
|
@@ -7953,6 +8112,7 @@ async function runFullVerification(configPath) {
|
|
|
7953
8112
|
}
|
|
7954
8113
|
const declaredMeshDocs = new Set(Object.keys(typedConfig.mesh ?? {}));
|
|
7955
8114
|
displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
|
|
8115
|
+
await runCoupledFieldsLint(typedConfig, typedAnalysis);
|
|
7956
8116
|
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
7957
8117
|
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
7958
8118
|
return;
|
|
@@ -8393,4 +8553,4 @@ main().catch((error) => {
|
|
|
8393
8553
|
process.exit(1);
|
|
8394
8554
|
});
|
|
8395
8555
|
|
|
8396
|
-
//# debugId=
|
|
8556
|
+
//# debugId=43F6AECD3B4E1E6264756E2164756E21
|