@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.
@@ -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=A59978709BAED66964756E2164756E21
8556
+ //# debugId=43F6AECD3B4E1E6264756E2164756E21