Skip to content

Publish a digest of the rule set this package compiles in - #222

Merged
patchstackdave merged 1 commit into
mainfrom
feat/publish-a-digest-of-the-shipped-rule-set
Sep 8, 2026
Merged

patchstackdave merged 1 commit into
mainfrom
feat/publish-a-digest-of-the-shipped-rule-set

Conversation

@patchstackdave

@patchstackdave patchstackdave commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Why

This package compiles a set of rules in, and the platform serves the same policies as documents it can
revise. Both copies exist deliberately: one protects an app before it is enrolled, the other can be
corrected without a release. They are only useful together if they agree, and nothing mechanical says
whether they do.

What this adds

scripts/rule-set-digest.mjs — a development script, not part of the published package and not on the
request path. It prints a digest per rule and one over the whole set, so checking agreement is a
comparison of two values instead of a reading of two files. --canonical prints the material behind the
digest, because a digest reports only that two things differ while whoever has to fix it needs to know
how.

It is a comparison projection, not a behaviour-parity proof

The compared fields are RULE_PROPERTIES minus an explicit exclusion allowlist of five — id,
rule_id, title, source_revision, enforcement — each carrying its reason where it sits. They are
excluded because the two copies are meant to differ on them, not because they have no effect:
enforcement decides whether a matching rule acts at all, and title becomes the block message for a
rule that declares no message. So equal digests mean the copies agree on everything compared here, and
not that they behave identically.

Deriving the projection from the contract has two consequences: a property added to the contract is
compared by default, and a test holds the projection to the contract with a stated reason per exclusion,
so removing a field from the comparison has to be argued for rather than drifting in.

The form is stated, not delegated to an encoder

The other side of this comparison is another language, and the two encoders disagree on values the rule
contract accepts:

Value One encoder The other
cookie_flags: {} {} [] after an associative decode
-0 0 -0
1e-7 1e-7 1.0e-7
{"10":…,"2":…} keys ordered as text numeric-looking keys ordered as numbers

So the canonical text is written out: keys sorted as text at every depth, arrays in order, an empty
object as {}, only ", \ and the characters below a space escaped and those as \u00xx rather than
the short forms, and numbers only where they are safe integers.

Integrality is not the bound. 1e21 is an integer here and writes itself in exponent form, while an
implementation holding it as an integer writes the digits, and one converting it to a 64-bit integer
gets a different number altogether. Above 2^53 there is no shared spelling, so the form stops there.

A value the form cannot pin down is refused, not approximated. A digest is worth comparing only if
the other side would produce the same one; a value written differently there reports drift between
documents that agree.

An absent property is not an authored null

Only the properties a rule states appear in its material. The contract treats an absent property and
an authored null as different documents and refuses that null for every property outside
NULL_EXEMPT_PROPERTIES — so of two copies, the one authoring null can have its rule refused outright
while the other's runs on the engine default. Nothing is refused here either, so an exempt
capture: null is recorded as the declaration it is.

The set digest covers the terms of the comparison

The whole-set value hashes the projection and a material version alongside the rule digests. An
undeclared property contributes nothing to any rule's digest, and most of the compared properties are
declared by no rule this package ships — so over the rule digests alone, a comparator that stopped
comparing one of those would emit the identical set value.

Testing

32 cases. No digest value of the shipped rule set is asserted: a snapshot would need updating whenever
a rule legitimately changes, and a test that must be edited to keep passing stops describing anything.
The properties are asserted of it instead — every shipped rule covered, digests distinct, the digest
moves when any compared field moves or is removed, distinguishes absent from authored null, holds
across key order and rule order, moves on clause reorder, stays aligned with RULE_PROPERTIES, and
never lets a rule declaring nothing agree with a rule declaring a value.

Version 2 is pinned by a fixed synthetic vector, since relational properties survive a changed hash, a
changed truncation and a changed serialisation while breaking the other implementation. The vector
carries nested key ordering, numeric-looking keys, array order, a slash and a backslash inside an
operand, non-ASCII text, control characters, an empty object, an integer, a boolean, an authored null and
absence — and is pinned twice: the canonical text is written out in the test independently of the code, and the bytes are fixed.
The aggregate vector uses a fixed projection, so a property added to the contract does not send anyone
editing a pinned value.

The vector is checked against enforceableRuleProblem, the validator the runtime itself uses, so the
format is not pinned against a document no guard would accept. --canonical output is checked to hash
to the digest reported for that rule, which is what makes it the material rather than a description of
it.

Full suite: 2512 passing, 7 skipped. typecheck, build and git diff --check clean.

@coderbuds

coderbuds Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Comprehensive script and tests deliver a robust rule-set digest feature.

🎯 Quality: 100% Elite · 📦 Size: Medium

📈 This month: Your 195th PR — above team average · Averaging Excellent

See how your team is trending →

@patchstackdave
patchstackdave force-pushed the feat/publish-a-digest-of-the-shipped-rule-set branch from ae18193 to 2866680 Compare September 7, 2026 14:16
@patchstackdave patchstackdave reopened this Sep 7, 2026
@patchstackdave
patchstackdave force-pushed the feat/publish-a-digest-of-the-shipped-rule-set branch 6 times, most recently from 1858993 to 5356f4f Compare September 7, 2026 15:01
@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave force-pushed the feat/publish-a-digest-of-the-shipped-rule-set branch 2 times, most recently from 0026624 to b45fc88 Compare September 7, 2026 17:34
The guard ships rules and the platform serves the same policies as documents it
can revise. Both copies exist on purpose — one protects an app before it is
enrolled, the other can be corrected without a release — and they are only
useful together if they agree. Nothing mechanical says whether they do.

`scripts/rule-set-digest.mjs` prints a digest per rule and one over the whole
set, so agreement is a comparison of two values rather than a reading of two
files. `--canonical` prints the material behind it, because a digest says "these
differ" and nothing else while whoever has to fix it needs the difference.

What is compared is derived from the rule contract rather than listed by hand:
every property in `RULE_PROPERTIES` except an explicit allowlist of five, each
with its reason stated where it is excluded. A property added to the contract is
therefore compared by default, and a test holds the projection to the contract so
an exclusion has to be argued for rather than drifting in.

Those five are excluded because the copies are meant to differ on them, not
because they do not affect behaviour: `enforcement` decides whether a rule acts,
and `title` becomes the block message for a rule declaring no `message`. Equal
digests therefore mean the copies agree on everything compared — a policy
projection — and not that they behave identically. The comparison is set-to-set
on those digests, which needs no shared identifier between the copies.

The material is TEXT, and its form is written out here rather than delegated to
`JSON.stringify`, because the other side of this comparison is another language
and the two encoders disagree on values the contract accepts:

  - an empty object is indistinguishable from an empty array to an associative
    decode, and `cookie_flags: {}` is a document somebody may write;
  - one encoder keeps the sign on negative zero and the other drops it;
  - `1e-7` is written `1e-7` by one and `1.0e-7` by the other.

So the form is the agreement: keys sorted at every depth, arrays in order, an
empty object as `{}`, only `"`, `\` and the characters below a space escaped and
those as `\u00xx` rather than the short forms, and numbers only where they are
SAFE integers. Integrality is not enough — `1e21` is an integer here and writes
itself in exponent form, while an implementation holding it as an integer writes
the digits and one converting it to a 64-bit integer gets a different number
altogether. Above 2^53 there is no shared spelling, so the form stops there.

A value the form cannot pin down is REFUSED rather than approximated. A digest is
worth comparing only if the other side would produce the same one, so a value
written differently there would report drift between documents that agree.
Positively or not at all.

Only the properties a rule states appear in its material. The contract treats an
absent property and an authored `null` as different documents, and refuses that
null for all but the exempt properties — so of two copies, the one authoring
`null` can have its rule refused while the other's runs on the engine default.
Standing an absent property in as a null would report those two as agreeing.
Nothing is refused here either, so an exempt `capture: null` is recorded as the
declaration it is.

Key order does not affect a digest and array order does: the same rule typed in a
different order is the same rule, while `rule_v2` is a sequence whose order
changes what a rule does and what it costs to evaluate.

The whole-set digest covers the projection and a version of the material as well
as the rule digests. A property no rule declares contributes nothing to any rule's
digest, so over those alone a comparator that stopped comparing such a property
would produce the identical value — and most of the compared properties are
declared by no rule this package ships, so that is the normal case rather than an
edge of it. Both lists are sorted, because a set has no order and two comparators
covering the same properties in a different order are comparing the same things.

No digest value of the shipped rule set is asserted in the tests. A snapshot of it
would have to be updated whenever a rule legitimately changes, and a test that
must be edited to keep passing stops describing anything. The properties are
asserted of it instead: the digest covers each compared property, moves when one
moves or is removed, holds across key order and rule order, and — stated directly,
because the removal case cannot see it — never lets a rule declaring nothing agree
with a rule declaring a value.

Because this is an interoperability format, its version is pinned by a fixed
synthetic vector as well. The vector carries nested key ordering, array order, a
slash and a backslash inside an operand, non-ASCII text, control characters, an
empty object, an integer, a boolean, an authored null and absence — and it is
pinned twice: the canonical text is written out in the test independently of the
code, and the bytes it hashes to are fixed. The aggregate vector uses a fixed
projection, so a property added to the contract does not send anyone editing a
pinned value: what is fixed is the algorithm, not this package's rule set.

The fixture values are checked against `enforceableRuleProblem`, the validator the
runtime itself uses, so a vector cannot describe a document no guard would accept.

Not on the request path and not in the published package. Every supported
invocation either emits the report or fails loudly: the absent-argument case is
answered before any path is resolved, so the module stays importable, and a named
script is compared by resolved real path, so a link or a path needing encoding
runs rather than exiting 0 having printed nothing. An entry that will not resolve
is an import, because a run of a path node cannot resolve never reaches any module.
@patchstackdave
patchstackdave force-pushed the feat/publish-a-digest-of-the-shipped-rule-set branch from b45fc88 to c046ef7 Compare September 8, 2026 06:32
@patchstackdave
patchstackdave merged commit a9acbb8 into main Sep 8, 2026
14 checks passed
@patchstackdave
patchstackdave deleted the feat/publish-a-digest-of-the-shipped-rule-set branch September 8, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants