@variance-authority/core 0.8.1 → 0.10.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +47 -21
  3. package/dist/attribute/boundary.d.ts +21 -14
  4. package/dist/attribute/boundary.js +23 -29
  5. package/dist/attribute/boundary.js.map +1 -1
  6. package/dist/attribute/component-hash.d.ts +21 -0
  7. package/dist/attribute/component-hash.js +48 -3
  8. package/dist/attribute/component-hash.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -3
  10. package/dist/attribute/index.js +1 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/lexicon.d.ts +37 -0
  13. package/dist/attribute/lexicon.js +40 -12
  14. package/dist/attribute/lexicon.js.map +1 -1
  15. package/dist/attribute/rename.d.ts +18 -0
  16. package/dist/attribute/rename.js +24 -0
  17. package/dist/attribute/rename.js.map +1 -0
  18. package/dist/compare/cascade.d.ts +2 -0
  19. package/dist/compare/cascade.js +5 -1
  20. package/dist/compare/cascade.js.map +1 -1
  21. package/dist/format/hash.d.ts +20 -0
  22. package/dist/format/hash.js +27 -1
  23. package/dist/format/hash.js.map +1 -1
  24. package/dist/format/index.d.ts +1 -1
  25. package/dist/format/index.js +1 -1
  26. package/dist/format/index.js.map +1 -1
  27. package/dist/format/snapshot.d.ts +15 -0
  28. package/dist/format/snapshot.js.map +1 -1
  29. package/dist/judge/sensitivity.js +1 -2
  30. package/dist/judge/sensitivity.js.map +1 -1
  31. package/dist/relate/graph.d.ts +10 -0
  32. package/dist/relate/graph.js +11 -1
  33. package/dist/relate/graph.js.map +1 -1
  34. package/dist/relate/index.d.ts +1 -0
  35. package/dist/relate/index.js +1 -0
  36. package/dist/relate/index.js.map +1 -1
  37. package/dist/relate/narrow.d.ts +53 -0
  38. package/dist/relate/narrow.js +133 -0
  39. package/dist/relate/narrow.js.map +1 -0
  40. package/dist/relate/reach.d.ts +7 -0
  41. package/dist/relate/reach.js +5 -0
  42. package/dist/relate/reach.js.map +1 -1
  43. package/dist/relate/records.d.ts +10 -1
  44. package/dist/relate/records.js +19 -2
  45. package/dist/relate/records.js.map +1 -1
  46. package/dist/share/http-line.d.ts +30 -0
  47. package/dist/share/http-line.js +164 -0
  48. package/dist/share/http-line.js.map +1 -0
  49. package/dist/share/index.d.ts +9 -126
  50. package/dist/share/index.js +9 -168
  51. package/dist/share/index.js.map +1 -1
  52. package/dist/share/line.d.ts +153 -0
  53. package/dist/share/line.js +184 -0
  54. package/dist/share/line.js.map +1 -0
  55. package/dist/share/publish.d.ts +79 -0
  56. package/dist/share/publish.js +231 -0
  57. package/dist/share/publish.js.map +1 -0
  58. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;AAClF,CAAC","sourcesContent":["import { canonicalize, type CanonicalValue } from './canonical.js';\nimport { sha256Hex, sha256HexBytes } from './sha256.js';\n\n/**\n * Content addressing (Principle 4).\n *\n * Hashes are the system's only identity. A baseline is addressed by what it *is*,\n * never by the branch or commit it arrived on, which is why a rebase cannot\n * invalidate one.\n *\n * Digests are prefixed with their algorithm and truncated to 128 bits. The prefix\n * makes a future algorithm change a visible, greppable migration rather than a\n * silent collision domain; 128 bits is far beyond collision risk for a corpus\n * bounded by the number of subjects a repository has, and keeps `variance.lock`\n * readable as a diff — a manifest a human refuses to read is a manifest nobody\n * reviews.\n */\nexport type Digest = string;\n\nconst PREFIX = 'v1';\nconst HEX_LENGTH = 32;\n\n/**\n * Hash an already-canonical string.\n *\n * Synchronous, and must stay so. `propsDigest` runs inside the rendering page,\n * where a prop may be a function or an element that cannot be serialized out —\n * so the digest is taken where the value still exists. `crypto.subtle` is async\n * and would colour the whole normalizer; see `sha256.ts`.\n */\nexport function digestString(input: string): Digest {\n return `${PREFIX}:${sha256Hex(input).slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * The digest of a SHA-256 some other implementation already computed.\n *\n * The algorithm in `sha256.ts` is portable TypeScript because `core` has to run\n * inside the page. A host that is not a page — a transform, a scanner, anything\n * reading files off a disk — has the same hash natively and an order of\n * magnitude faster, and FIPS 180-4 leaves no room for the two to disagree.\n *\n * What such a host must not also own is the shape. The prefix is what makes an\n * algorithm change a greppable migration, and the truncation is what keeps a\n * manifest readable; a second place deciding either is a digest domain that\n * splits without anybody noticing. So the host brings the hex and this brings\n * the identity.\n */\nexport function digestOfSha256(hex: string): Digest {\n return `${PREFIX}:${hex.slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * Hash raw bytes — an image, a font file, anything whose identity is its octets.\n *\n * Same prefix and same truncation as every other digest here, so an asset hash\n * and a render hash are the same kind of value and neither can be mistaken for\n * the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not\n * an identity, and the same `url(...)` can resolve to different bytes tomorrow.\n */\nexport function digestBytes(bytes: Uint8Array): Digest {\n return `${PREFIX}:${sha256HexBytes(bytes).slice(0, HEX_LENGTH)}`;\n}\n\n/** Canonicalize then hash. The only correct way to hash a structure. */\nexport function digestValue(value: CanonicalValue): Digest {\n return digestString(canonicalize(value));\n}\n\n/**\n * Combine digests under a domain label.\n *\n * The label prevents cross-domain collision: an environment key and a render hash\n * built from identical component digests must not be equal, or an environment\n * change could masquerade as a content match.\n */\nexport function digestCombine(domain: string, parts: readonly Digest[]): Digest {\n return digestString(`${domain}\\u0000${parts.join('\\u0000')}`);\n}\n\n/**\n * How much of an encoded id a filename may carry.\n *\n * A filename is capped at 255 bytes on every filesystem anybody runs a suite on,\n * and a caller's own suffix -- an extension, a temporary marker with a pid and a\n * UUID in it -- comes out of the same 255. What is left is the budget.\n */\nconst NAME_BUDGET = 140;\n\n/**\n * A filesystem name for an id that was not written with filesystems in mind.\n *\n * An id long enough to overrun a filename is not an exotic case: a suite that\n * names subjects after the test that produced them -- a file path and a full\n * test name -- passes 255 bytes on ordinary tests, and the whole point of that\n * convention is that the id says where the subject came from. Truncating alone\n * would put two tests in one file, so a long id keeps a readable prefix and\n * earns a digest of the whole id, which is what actually distinguishes it.\n */\nexport function fileNameFor(id: string): string {\n const encoded = encodeURIComponent(id);\n if (encoded.length <= NAME_BUDGET) return encoded;\n return `${encoded.slice(0, NAME_BUDGET)}~${digestString(id).replace(':', '-')}`;\n}\n"]}
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,cAAc,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,iBAAiB,UAAU,KAAK,CAAC,CAAC;AAEzE;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACtC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AAC5D,CAAC","sourcesContent":["import { canonicalize, type CanonicalValue } from './canonical.js';\nimport { sha256Hex, sha256HexBytes } from './sha256.js';\n\n/**\n * Content addressing (Principle 4).\n *\n * Hashes are the system's only identity. A baseline is addressed by what it *is*,\n * never by the branch or commit it arrived on, which is why a rebase cannot\n * invalidate one.\n *\n * Digests are prefixed with their algorithm and truncated to 128 bits. The prefix\n * makes a future algorithm change a visible, greppable migration rather than a\n * silent collision domain; 128 bits is far beyond collision risk for a corpus\n * bounded by the number of subjects a repository has, and keeps `variance.lock`\n * readable as a diff — a manifest a human refuses to read is a manifest nobody\n * reviews.\n */\nexport type Digest = string;\n\nconst PREFIX = 'v1';\nconst HEX_LENGTH = 32;\n\n/**\n * Hash an already-canonical string.\n *\n * Synchronous, and must stay so. `propsDigest` runs inside the rendering page,\n * where a prop may be a function or an element that cannot be serialized out —\n * so the digest is taken where the value still exists. `crypto.subtle` is async\n * and would colour the whole normalizer; see `sha256.ts`.\n */\nexport function digestString(input: string): Digest {\n return `${PREFIX}:${sha256Hex(input).slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * The digest of a SHA-256 some other implementation already computed.\n *\n * The algorithm in `sha256.ts` is portable TypeScript because `core` has to run\n * inside the page. A host that is not a page — a transform, a scanner, anything\n * reading files off a disk — has the same hash natively and an order of\n * magnitude faster, and FIPS 180-4 leaves no room for the two to disagree.\n *\n * What such a host must not also own is the shape. The prefix is what makes an\n * algorithm change a greppable migration, and the truncation is what keeps a\n * manifest readable; a second place deciding either is a digest domain that\n * splits without anybody noticing. So the host brings the hex and this brings\n * the identity.\n */\nexport function digestOfSha256(hex: string): Digest {\n return `${PREFIX}:${hex.slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * Hash raw bytes — an image, a font file, anything whose identity is its octets.\n *\n * Same prefix and same truncation as every other digest here, so an asset hash\n * and a render hash are the same kind of value and neither can be mistaken for\n * the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not\n * an identity, and the same `url(...)` can resolve to different bytes tomorrow.\n */\nexport function digestBytes(bytes: Uint8Array): Digest {\n return `${PREFIX}:${sha256HexBytes(bytes).slice(0, HEX_LENGTH)}`;\n}\n\n/** Canonicalize then hash. The only correct way to hash a structure. */\nexport function digestValue(value: CanonicalValue): Digest {\n return digestString(canonicalize(value));\n}\n\n/**\n * Combine digests under a domain label.\n *\n * The label prevents cross-domain collision: an environment key and a render hash\n * built from identical component digests must not be equal, or an environment\n * change could masquerade as a content match.\n */\nexport function digestCombine(domain: string, parts: readonly Digest[]): Digest {\n return digestString(`${domain}\\u0000${parts.join('\\u0000')}`);\n}\n\n/**\n * How much of an encoded id a filename may carry.\n *\n * A filename is capped at 255 bytes on every filesystem anybody runs a suite on,\n * and a caller's own suffix -- an extension, a temporary marker with a pid and a\n * UUID in it -- comes out of the same 255. What is left is the budget.\n */\nconst NAME_BUDGET = 140;\n\n/**\n * A filesystem name for an id that was not written with filesystems in mind.\n *\n * An id long enough to overrun a filename is not an exotic case: a suite that\n * names subjects after the test that produced them -- a file path and a full\n * test name -- passes 255 bytes on ordinary tests, and the whole point of that\n * convention is that the id says where the subject came from. Truncating alone\n * would put two tests in one file, so a long id keeps a readable prefix and\n * earns a digest of the whole id, which is what actually distinguishes it.\n */\nexport function fileNameFor(id: string): string {\n const encoded = encodeURIComponent(id);\n if (encoded.length <= NAME_BUDGET) return encoded;\n return `${encoded.slice(0, NAME_BUDGET)}~${digestFileName(digestString(id))}`;\n}\n\n/**\n * A digest spelled so that a filesystem accepts it: `v1-<hex>` for `v1:<hex>`.\n *\n * The colon is the one character in a digest that is not portable. NTFS refuses\n * it in a name, so a directory named by a raw digest is a repository that cannot\n * be checked out on Windows, and `actions/upload-artifact` refuses the path for\n * the same reason. Every name made from a digest goes through here, so the\n * spelling has one owner and {@link digestOfFileName} can read it back.\n */\nexport function digestFileName(digest: Digest): string {\n return digest.replace(':', '-');\n}\n\nconst FILE_NAME = new RegExp(`^${PREFIX}[-:]([0-9a-f]{${HEX_LENGTH}})$`);\n\n/**\n * The digest a name spells, or `undefined` because it spells none.\n *\n * Reads the raw `v1:<hex>` spelling as well as {@link digestFileName}'s. The\n * file-backed store named its identity partitions with the raw digest before\n * this spelling existed, and a baseline kept under that name is still a\n * baseline: a reader that knew only the new spelling would call it `new` and\n * record over it.\n */\nexport function digestOfFileName(name: string): Digest | undefined {\n const hex = FILE_NAME.exec(name)?.[1];\n return hex === undefined ? undefined : `${PREFIX}:${hex}`;\n}\n"]}
@@ -16,7 +16,7 @@ export { canonicalize, canonicalNumber } from './canonical.js';
16
16
  export type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';
17
17
  export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
18
18
  export type { Digest } from './hash.js';
19
- export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
19
+ export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
20
20
  export type { AccessibilitySnapshot } from './accessibility.js';
21
21
  export { accessibilitySnapshot } from './accessibility.js';
22
22
  export type { ObservationProfile, ProfileId } from './profile.js';
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { canonicalize, canonicalNumber } from './canonical.js';
15
15
  export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
16
- export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
16
+ export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
17
17
  export { accessibilitySnapshot } from './accessibility.js';
18
18
  export { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';
19
19
  export { tierReaches } from './tier.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG/G,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGrG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAWhD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * `@variance-authority/core/format` — what a subject *is*, as a value.\n *\n * The shapes every other group reads and none of them may redefine: a raw\n * capture as a collector hands it over, the normalized snapshot it becomes, the\n * document a renderer paints, the identity that addresses the result, and the\n * hashing that makes any of it comparable.\n *\n * A consumer writing their own collector needs exactly this and nothing else.\n * That is the entrypoint's reason to exist: the format is a contract other people\n * implement, and implementing it should not require reading the rules that\n * interpret it or the verdicts drawn from it.\n */\n\n// compass: variance-authority.normalization\n\nexport type { CanonicalValue } from './canonical.js';\nexport { canonicalize, canonicalNumber } from './canonical.js';\n\nexport type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';\nexport { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';\n\nexport type { Digest } from './hash.js';\nexport { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';\n\nexport type { AccessibilitySnapshot } from './accessibility.js';\nexport { accessibilitySnapshot } from './accessibility.js';\n\nexport type { ObservationProfile, ProfileId } from './profile.js';\nexport { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';\n\nexport type { Tier } from './tier.js';\nexport { tierReaches } from './tier.js';\n\nexport type {\n EnvironmentInputs,\n EnvironmentKey,\n EnvironmentDelta,\n EnvironmentField,\n Viewport,\n} from './environment.js';\nexport { environmentKey, diffEnvironments } from './environment.js';\n\nexport {\n INTERVENTIONS,\n SEMANTIC_RECIPE,\n COLLECT_RECIPE,\n LAYOUT_RECIPE,\n RASTER_RECIPE,\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n waitForFonts,\n waitForImages,\n forTier,\n conflicts,\n interventionById,\n recipeOf,\n recipeCss,\n recipeScreenshot,\n recipeDigest,\n settleRecipe,\n describeRecipe,\n} from './stabilize.js';\nexport type {\n Intervention,\n Recipe,\n Trick,\n ScreenshotOptions,\n SettleTarget,\n} from './stabilize.js';\n\nexport type { Provenance, OwnerFrame, SourceLocation, StackFrame } from './provenance.js';\nexport { propsDigest, heldDigest, jsxSourceOf, relativizeSource, JSX_SOURCE } from './provenance.js';\nexport type { Holding, HeldCell, HeldValue } from './holding.js';\nexport type { Wiring } from './wiring.js';\nexport { keyedByPosition } from './wiring.js';\n\nexport type {\n RawCapture,\n RawNode,\n RawAria,\n MatchedRule,\n Declaration,\n Rect,\n SubjectRef,\n Diagnostic,\n} from './capture.js';\nexport { mergeDiagnostics } from './capture.js';\n\nexport type {\n SemanticSnapshot,\n SemanticNode,\n NodePath,\n StyleProvenanceEntry,\n IgnoreSite,\n ComponentHash,\n} from './snapshot.js';\n\nexport { documentDigest, identityDigest, occupiesPixels, pictured, picture } from './document.js';\nexport type {\n RenderDocument,\n RenderFrame,\n FrameElement,\n RenderResource,\n RenderIdentity,\n Raster,\n Pictured,\n} from './document.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGjJ,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGrG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAWhD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * `@variance-authority/core/format` — what a subject *is*, as a value.\n *\n * The shapes every other group reads and none of them may redefine: a raw\n * capture as a collector hands it over, the normalized snapshot it becomes, the\n * document a renderer paints, the identity that addresses the result, and the\n * hashing that makes any of it comparable.\n *\n * A consumer writing their own collector needs exactly this and nothing else.\n * That is the entrypoint's reason to exist: the format is a contract other people\n * implement, and implementing it should not require reading the rules that\n * interpret it or the verdicts drawn from it.\n */\n\n// compass: variance-authority.normalization\n\nexport type { CanonicalValue } from './canonical.js';\nexport { canonicalize, canonicalNumber } from './canonical.js';\n\nexport type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';\nexport { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';\n\nexport type { Digest } from './hash.js';\nexport { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';\n\nexport type { AccessibilitySnapshot } from './accessibility.js';\nexport { accessibilitySnapshot } from './accessibility.js';\n\nexport type { ObservationProfile, ProfileId } from './profile.js';\nexport { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';\n\nexport type { Tier } from './tier.js';\nexport { tierReaches } from './tier.js';\n\nexport type {\n EnvironmentInputs,\n EnvironmentKey,\n EnvironmentDelta,\n EnvironmentField,\n Viewport,\n} from './environment.js';\nexport { environmentKey, diffEnvironments } from './environment.js';\n\nexport {\n INTERVENTIONS,\n SEMANTIC_RECIPE,\n COLLECT_RECIPE,\n LAYOUT_RECIPE,\n RASTER_RECIPE,\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n waitForFonts,\n waitForImages,\n forTier,\n conflicts,\n interventionById,\n recipeOf,\n recipeCss,\n recipeScreenshot,\n recipeDigest,\n settleRecipe,\n describeRecipe,\n} from './stabilize.js';\nexport type {\n Intervention,\n Recipe,\n Trick,\n ScreenshotOptions,\n SettleTarget,\n} from './stabilize.js';\n\nexport type { Provenance, OwnerFrame, SourceLocation, StackFrame } from './provenance.js';\nexport { propsDigest, heldDigest, jsxSourceOf, relativizeSource, JSX_SOURCE } from './provenance.js';\nexport type { Holding, HeldCell, HeldValue } from './holding.js';\nexport type { Wiring } from './wiring.js';\nexport { keyedByPosition } from './wiring.js';\n\nexport type {\n RawCapture,\n RawNode,\n RawAria,\n MatchedRule,\n Declaration,\n Rect,\n SubjectRef,\n Diagnostic,\n} from './capture.js';\nexport { mergeDiagnostics } from './capture.js';\n\nexport type {\n SemanticSnapshot,\n SemanticNode,\n NodePath,\n StyleProvenanceEntry,\n IgnoreSite,\n ComponentHash,\n} from './snapshot.js';\n\nexport { documentDigest, identityDigest, occupiesPixels, pictured, picture } from './document.js';\nexport type {\n RenderDocument,\n RenderFrame,\n FrameElement,\n RenderResource,\n RenderIdentity,\n Raster,\n Pictured,\n} from './document.js';\n"]}
@@ -247,6 +247,21 @@ export interface ComponentHash {
247
247
  * kept in place so the list stays parallel to the instances it counts.
248
248
  */
249
249
  readonly boxes?: readonly (Rect | null)[];
250
+ /**
251
+ * Every distinct value the component's own nodes declared, per property, in
252
+ * code-unit order; a custom property appears under its `--name` with the
253
+ * values it resolved to. Never hashed.
254
+ *
255
+ * `boxes` answers *by how much*; this answers *which declaration*. `style`
256
+ * moved says a `Button` is not styled as it was, and a reviewer holding only
257
+ * that opens the file and diffs by eye. With both revisions' values the run
258
+ * says `padding-left 8px → 14px`, which is the line somebody edited.
259
+ *
260
+ * Absent for a baseline written before this existed. That means *nobody
261
+ * recorded it*, and the comparison says nothing about properties rather than
262
+ * reporting every one of them as new.
263
+ */
264
+ readonly values?: Readonly<Record<string, readonly string[]>>;
250
265
  }
251
266
  export interface StyleProvenanceEntry {
252
267
  readonly path: NodePath;
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n\n /**\n * Every distinct value the component's own nodes declared, per property, in\n * code-unit order; a custom property appears under its `--name` with the\n * values it resolved to. Never hashed.\n *\n * `boxes` answers *by how much*; this answers *which declaration*. `style`\n * moved says a `Button` is not styled as it was, and a reviewer holding only\n * that opens the file and diffs by eye. With both revisions' values the run\n * says `padding-left 8px → 14px`, which is the line somebody edited.\n *\n * Absent for a baseline written before this existed. That means *nobody\n * recorded it*, and the comparison says nothing about properties rather than\n * reporting every one of them as new.\n */\n readonly values?: Readonly<Record<string, readonly string[]>>;\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
@@ -60,8 +60,7 @@ export function summarizeSensitivity(register) {
60
60
  for (const entry of register.relaxations) {
61
61
  if (entry.absorbed === 0) {
62
62
  lines.push(` [dead] ${entry.rule} — asserts on ${entry.level} across ` +
63
- `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason}); ` +
64
- 'nothing here needed relaxing');
63
+ `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason})`);
65
64
  continue;
66
65
  }
67
66
  lines.push(` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +
@@ -1 +1 @@
1
- {"version":3,"file":"sensitivity.js","sourceRoot":"","sources":["../../src/judge/sensitivity.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { SemanticDiff } from '../compare/diff/index.js';\nimport { applyIgnores, type IgnoreOptions, type IgnoreRule } from './ignore.js';\n\n/**\n * How much of a subject is being asserted on, declared where it applies.\n *\n * A route-level test and a component-level test want opposite things from the\n * same machinery. A component's test asserts on everything: a colour token moved\n * and that is the change. A route's test asserts that the *page still assembles*\n * — the nav is where it was, the sidebar did not collapse, nothing overlaps — and\n * a design-system token landing in forty routes is noise it should never have\n * been shown. Run one policy over both and one of them is unusable: either the\n * routes are permanently red, or the components stop reporting the thing they\n * exist to catch.\n *\n * The usual answer is a second product, or a threshold, and both are worse than\n * the problem. What this offers instead is the same shape an\n * {@link IgnoreRule} has, because it is the same act: **naming what is not being\n * asserted on, where, by whom, and with the count in the report.**\n *\n * ## The unit is a band, and that is the whole design\n *\n * The frequency bands already say what *kind* of thing moved — `a11y`,\n * `geometry`, `token`, `content`, `texture` — and they are ordered by how rarely\n * they move and how much it matters when they do. \"Assert on layout and ignore\n * small token updates\" is not a tolerance, a percentage or a pixel count. It is\n * two band names.\n *\n * That is why this is not a threshold: a threshold absorbs *anything* small\n * enough, and a band absorbs exactly one kind of thing however large it is. A\n * route declared `layout` still reports a nav that moved by one pixel, and never\n * reports a rebrand that repainted every surface on the page.\n *\n * ## Why it is a declaration and not a mode\n *\n * The same reason an ignore is. A run-wide switch is a decision nobody attributes\n * and nobody revisits; a rule with an id, a required reason and a place is one a\n * reviewer can read six months later and disagree with. Everything an ignore owes\n * it owes: a reason, a scope, and a count of what it absorbed — including the\n * count of zero, which is how a declaration that has stopped being true is found.\n */\n\n/**\n * A named set of bands worth asserting on.\n *\n * A closed set. Three names cover the cases that have argued for themselves and\n * a fourth would need one; an open set of user-defined levels would put the\n * vocabulary in a config file, where the report cannot explain what a word meant\n * to whoever wrote it.\n */\nexport type Level =\n /**\n * Everything. What a component's own test wants, and the default everywhere.\n *\n * Named rather than left implicit so a subject can be declared strict *back*\n * inside a route that is not — the exception a broad declaration always grows.\n */\n | 'strict'\n /**\n * The page still assembles: `a11y` and `geometry`.\n *\n * `geometry` is the obvious half — boxes appeared, vanished, moved or resized.\n * `a11y` is the half that gets left out and should not be: a control that lost\n * its accessible name is a structural regression that happens to repaint\n * nothing, and a route test that ignored it would be asserting on the shape of\n * the page while blind to the shape a screen reader sees.\n *\n * What it absorbs is `token`, `content` and `texture` — a restyle, a copy edit,\n * anti-aliasing. Each is a real change, each is reported as absorbed, and none\n * of them is what a route-level test is for.\n */\n | 'layout'\n /**\n * What the page *says*: `a11y` and `content`.\n *\n * For a subject whose styling is somebody else's to change — a themed embed, a\n * page inside a design system being actively rebranded — where the assertion\n * worth keeping is that the words and the semantics survived.\n */\n | 'content';\n\nconst LEVELS: Readonly<Record<Level, readonly Band[]>> = {\n strict: BANDS,\n layout: ['a11y', 'geometry'],\n content: ['a11y', 'content'],\n};\n\n/** The bands a level asserts on. Everything else is absorbed and counted. */\nexport function bandsOf(level: Level): readonly Band[] {\n return LEVELS[level];\n}\n\nexport interface SensitivityRule {\n /** Stable name. Appears in the report and in every count this rule produces. */\n readonly id: string;\n\n /**\n * Why this subject is not asserted on in full. Required, as an ignore's is.\n *\n * The difference between a policy somebody chose and one that accreted. A\n * route declared `layout` two years ago by somebody who has left is a blind\n * spot with a plausible-looking config entry in front of it.\n */\n readonly reason: string;\n\n readonly level: Level;\n\n /** Subjects this applies to. `*` matches any run of characters. */\n readonly subjects?: readonly string[];\n\n /**\n * Tags the subject must carry, as its own artifact declared them.\n *\n * FIXME: read by nothing. `asIgnore` copies `subjects` and drops this, so a\n * rule scoped only by tags translates to an unscoped ignore and absorbs across\n * every subject in the run — the widest possible reading of the narrowest\n * scope an operator can write.\n */\n readonly tags?: readonly string[];\n}\n\n/** What one rule absorbed, per band, across a run. */\nexport interface Relaxation {\n readonly rule: string;\n readonly reason: string;\n readonly level: Level;\n\n /** Deltas absorbed because their band is not asserted on here. */\n readonly absorbed: number;\n readonly subjects: readonly string[];\n\n /** Which bands were absorbed, so a level can be tightened with evidence. */\n readonly bands: readonly Band[];\n}\n\nexport interface SensitivityRegister {\n readonly relaxations: readonly Relaxation[];\n\n /**\n * Rules that absorbed nothing in any subject they applied to.\n *\n * The same accounting an ignore's register keeps, and for the same reason: a\n * route declared `layout` that has reported no token change in six months is\n * either a route nothing styles, or a declaration nobody needed. Both are worth\n * a line, and neither is visible without the count.\n */\n readonly dead: readonly string[];\n\n readonly totalAbsorbed: number;\n}\n\nexport interface SensitivityOutcome {\n readonly diffs: readonly SemanticDiff[];\n readonly register: SensitivityRegister;\n}\n\n/**\n * Turn a sensitivity rule into the ignore it is.\n *\n * Not a shortcut — a statement about what these are. A sensitivity names a place\n * and narrows by band, which is exactly the shape `IgnoreRule` already refuses to\n * accept *without* a place, and exactly the shape it accepts with one. Sharing\n * the mechanism means sharing the safety property: the deltas are absorbed by\n * `applyIgnores`, counted per rule, and reported when a rule absorbs nothing.\n *\n * The bands are inverted on the way through, and that inversion is the whole\n * translation. A sensitivity says what it *asserts on*; an ignore says what it\n * *absorbs*. Stating the level positively is what makes a config readable — \"this\n * route asserts on layout\" rather than \"this route ignores token, content and\n * texture\" — and the negative form is what the machinery needs.\n */\nexport function asIgnore(rule: SensitivityRule): IgnoreRule | null {\n const asserted = new Set(bandsOf(rule.level));\n const absorbed = BANDS.filter((band) => !asserted.has(band));\n\n // `strict` asserts on everything, so it absorbs nothing and produces no rule.\n // Returning an ignore with an empty band list would produce a register entry\n // that is permanently dead, which is a line telling an operator to delete the\n // one declaration that is doing nothing wrong.\n if (absorbed.length === 0) return null;\n\n return {\n id: rule.id,\n reason: rule.reason,\n bands: absorbed,\n // The place is the subject, said rather than left to be inferred from a\n // missing selector — see `IgnoreRule.whole`.\n whole: true,\n ...(rule.subjects !== undefined ? { subjects: rule.subjects } : {}),\n };\n}\n\n/**\n * The register as the lines a run prints.\n *\n * Absorption is stated in the *positive* form the operator wrote, because that is\n * the sentence they can check: \"asserts on layout\" is a claim about intent, and\n * \"absorbed 412 token deltas\" is the evidence for or against it.\n */\nexport function summarizeSensitivity(register: SensitivityRegister): readonly string[] {\n if (register.relaxations.length === 0) return [];\n\n const lines = [\n `SENSITIVITY — ${register.totalAbsorbed} difference(s) not asserted on, ` +\n `by ${register.relaxations.length} rule(s)`,\n ];\n\n for (const entry of register.relaxations) {\n if (entry.absorbed === 0) {\n lines.push(\n ` [dead] ${entry.rule} — asserts on ${entry.level} across ` +\n `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason}); ` +\n 'nothing here needed relaxing',\n );\n continue;\n }\n lines.push(\n ` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +\n `${entry.bands.join('/')} difference(s) in ${entry.subjects.length} subject(s): ` +\n entry.reason,\n );\n }\n\n return lines;\n}\n\n/**\n * Apply declared sensitivities to a run's diffs.\n *\n * A thin fold over {@link applyIgnores}, and thin on purpose: the absorption, the\n * per-rule counting and the dead-rule reporting are one mechanism, and a second\n * implementation of them would be a second set of bugs in the half of the system\n * whose whole job is not to silently hide things.\n *\n * Rules that resolve to `strict` are dropped before the fold rather than passed\n * through, so a project that declares its default explicitly does not collect a\n * permanently-dead register line for saying so.\n */\nexport function applySensitivity(\n diffs: readonly SemanticDiff[],\n rules: readonly SensitivityRule[],\n options: IgnoreOptions = {},\n): SensitivityOutcome {\n const translated = new Map<string, SensitivityRule>();\n const ignores: IgnoreRule[] = [];\n\n for (const rule of rules) {\n const ignore = asIgnore(rule);\n if (ignore === null) continue;\n translated.set(rule.id, rule);\n ignores.push(ignore);\n }\n\n const outcome = applyIgnores(diffs, ignores, options);\n\n const relaxations = [...translated.values()].map((rule) => {\n const absorbed = outcome.register.absorbed.find((entry) => entry.rule === rule.id);\n return {\n rule: rule.id,\n reason: rule.reason,\n level: rule.level,\n absorbed: absorbed?.deltas ?? 0,\n subjects: absorbed?.subjects ?? [],\n bands: absorbed?.bands ?? [],\n };\n });\n\n return {\n diffs: outcome.diffs,\n register: {\n relaxations,\n dead: relaxations.filter((entry) => entry.absorbed === 0).map((entry) => entry.rule),\n totalAbsorbed: relaxations.reduce((sum, entry) => sum + entry.absorbed, 0),\n },\n };\n}\n\n/**\n * What a level does with a set of bands that moved.\n *\n * The arm for the path that holds no second document. `applySensitivity` folds\n * over deltas, which needs both revisions normalized; a `variance run` compares\n * an image against a stored baseline and has only what that baseline carried —\n * per-component hashes, one digest per band (ADR-0027, ADR-0029's split).\n * `bandsBetween` turns those into the same vocabulary, and this decides against\n * it, so the two paths agree by sharing {@link bandsOf} rather than by\n * inspection.\n *\n * **Empty in means nothing absorbed**, not everything. A run whose pixels moved\n * while every component hash held is raster residue — `texture`, which a\n * document cannot carry and which this must therefore never claim to have\n * decided. Absorbing on an empty set would turn the one band a hash comparison\n * is blind to into the one it silences.\n */\nexport function relaxes(\n level: Level,\n moved: readonly Band[],\n): { readonly asserted: readonly Band[]; readonly absorbed: readonly Band[] } {\n const asserting = new Set(bandsOf(level));\n\n return {\n asserted: moved.filter((band) => asserting.has(band)),\n absorbed: moved.filter((band) => !asserting.has(band)),\n };\n}\n\n/**\n * Whether a subject's every moved band falls outside what it is asserted on.\n *\n * The whole subject, because that is the unit a stored baseline can answer for:\n * one image, one verdict. A subject where *some* moved band is asserted on is\n * reported in full — including the bands that would have been absorbed — since\n * a reviewer looking at a nav that moved wants the restyle that came with it,\n * and hiding half a diff is worse than hiding none of it.\n */\nexport function absorbsEntirely(level: Level, moved: readonly Band[]): boolean {\n return moved.length > 0 && relaxes(level, moved).asserted.length === 0;\n}\n"]}
1
+ {"version":3,"file":"sensitivity.js","sourceRoot":"","sources":["../../src/judge/sensitivity.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { SemanticDiff } from '../compare/diff/index.js';\nimport { applyIgnores, type IgnoreOptions, type IgnoreRule } from './ignore.js';\n\n/**\n * How much of a subject is being asserted on, declared where it applies.\n *\n * A route-level test and a component-level test want opposite things from the\n * same machinery. A component's test asserts on everything: a colour token moved\n * and that is the change. A route's test asserts that the *page still assembles*\n * — the nav is where it was, the sidebar did not collapse, nothing overlaps — and\n * a design-system token landing in forty routes is noise it should never have\n * been shown. Run one policy over both and one of them is unusable: either the\n * routes are permanently red, or the components stop reporting the thing they\n * exist to catch.\n *\n * The usual answer is a second product, or a threshold, and both are worse than\n * the problem. What this offers instead is the same shape an\n * {@link IgnoreRule} has, because it is the same act: **naming what is not being\n * asserted on, where, by whom, and with the count in the report.**\n *\n * ## The unit is a band, and that is the whole design\n *\n * The frequency bands already say what *kind* of thing moved — `a11y`,\n * `geometry`, `token`, `content`, `texture` — and they are ordered by how rarely\n * they move and how much it matters when they do. \"Assert on layout and ignore\n * small token updates\" is not a tolerance, a percentage or a pixel count. It is\n * two band names.\n *\n * That is why this is not a threshold: a threshold absorbs *anything* small\n * enough, and a band absorbs exactly one kind of thing however large it is. A\n * route declared `layout` still reports a nav that moved by one pixel, and never\n * reports a rebrand that repainted every surface on the page.\n *\n * ## Why it is a declaration and not a mode\n *\n * The same reason an ignore is. A run-wide switch is a decision nobody attributes\n * and nobody revisits; a rule with an id, a required reason and a place is one a\n * reviewer can read six months later and disagree with. Everything an ignore owes\n * it owes: a reason, a scope, and a count of what it absorbed — including the\n * count of zero, which is how a declaration that has stopped being true is found.\n */\n\n/**\n * A named set of bands worth asserting on.\n *\n * A closed set. Three names cover the cases that have argued for themselves and\n * a fourth would need one; an open set of user-defined levels would put the\n * vocabulary in a config file, where the report cannot explain what a word meant\n * to whoever wrote it.\n */\nexport type Level =\n /**\n * Everything. What a component's own test wants, and the default everywhere.\n *\n * Named rather than left implicit so a subject can be declared strict *back*\n * inside a route that is not — the exception a broad declaration always grows.\n */\n | 'strict'\n /**\n * The page still assembles: `a11y` and `geometry`.\n *\n * `geometry` is the obvious half — boxes appeared, vanished, moved or resized.\n * `a11y` is the half that gets left out and should not be: a control that lost\n * its accessible name is a structural regression that happens to repaint\n * nothing, and a route test that ignored it would be asserting on the shape of\n * the page while blind to the shape a screen reader sees.\n *\n * What it absorbs is `token`, `content` and `texture` — a restyle, a copy edit,\n * anti-aliasing. Each is a real change, each is reported as absorbed, and none\n * of them is what a route-level test is for.\n */\n | 'layout'\n /**\n * What the page *says*: `a11y` and `content`.\n *\n * For a subject whose styling is somebody else's to change — a themed embed, a\n * page inside a design system being actively rebranded — where the assertion\n * worth keeping is that the words and the semantics survived.\n */\n | 'content';\n\nconst LEVELS: Readonly<Record<Level, readonly Band[]>> = {\n strict: BANDS,\n layout: ['a11y', 'geometry'],\n content: ['a11y', 'content'],\n};\n\n/** The bands a level asserts on. Everything else is absorbed and counted. */\nexport function bandsOf(level: Level): readonly Band[] {\n return LEVELS[level];\n}\n\nexport interface SensitivityRule {\n /** Stable name. Appears in the report and in every count this rule produces. */\n readonly id: string;\n\n /**\n * Why this subject is not asserted on in full. Required, as an ignore's is.\n *\n * The difference between a policy somebody chose and one that accreted. A\n * route declared `layout` two years ago by somebody who has left is a blind\n * spot with a plausible-looking config entry in front of it.\n */\n readonly reason: string;\n\n readonly level: Level;\n\n /** Subjects this applies to. `*` matches any run of characters. */\n readonly subjects?: readonly string[];\n\n /**\n * Tags the subject must carry, as its own artifact declared them.\n *\n * FIXME: read by nothing. `asIgnore` copies `subjects` and drops this, so a\n * rule scoped only by tags translates to an unscoped ignore and absorbs across\n * every subject in the run — the widest possible reading of the narrowest\n * scope an operator can write.\n */\n readonly tags?: readonly string[];\n}\n\n/** What one rule absorbed, per band, across a run. */\nexport interface Relaxation {\n readonly rule: string;\n readonly reason: string;\n readonly level: Level;\n\n /** Deltas absorbed because their band is not asserted on here. */\n readonly absorbed: number;\n readonly subjects: readonly string[];\n\n /** Which bands were absorbed, so a level can be tightened with evidence. */\n readonly bands: readonly Band[];\n}\n\nexport interface SensitivityRegister {\n readonly relaxations: readonly Relaxation[];\n\n /**\n * Rules that absorbed nothing in any subject they applied to.\n *\n * The same accounting an ignore's register keeps, and for the same reason: a\n * route declared `layout` that has reported no token change in six months is\n * either a route nothing styles, or a declaration nobody needed. Both are worth\n * a line, and neither is visible without the count.\n */\n readonly dead: readonly string[];\n\n readonly totalAbsorbed: number;\n}\n\nexport interface SensitivityOutcome {\n readonly diffs: readonly SemanticDiff[];\n readonly register: SensitivityRegister;\n}\n\n/**\n * Turn a sensitivity rule into the ignore it is.\n *\n * Not a shortcut — a statement about what these are. A sensitivity names a place\n * and narrows by band, which is exactly the shape `IgnoreRule` already refuses to\n * accept *without* a place, and exactly the shape it accepts with one. Sharing\n * the mechanism means sharing the safety property: the deltas are absorbed by\n * `applyIgnores`, counted per rule, and reported when a rule absorbs nothing.\n *\n * The bands are inverted on the way through, and that inversion is the whole\n * translation. A sensitivity says what it *asserts on*; an ignore says what it\n * *absorbs*. Stating the level positively is what makes a config readable — \"this\n * route asserts on layout\" rather than \"this route ignores token, content and\n * texture\" — and the negative form is what the machinery needs.\n */\nexport function asIgnore(rule: SensitivityRule): IgnoreRule | null {\n const asserted = new Set(bandsOf(rule.level));\n const absorbed = BANDS.filter((band) => !asserted.has(band));\n\n // `strict` asserts on everything, so it absorbs nothing and produces no rule.\n // Returning an ignore with an empty band list would produce a register entry\n // that is permanently dead, which is a line telling an operator to delete the\n // one declaration that is doing nothing wrong.\n if (absorbed.length === 0) return null;\n\n return {\n id: rule.id,\n reason: rule.reason,\n bands: absorbed,\n // The place is the subject, said rather than left to be inferred from a\n // missing selector — see `IgnoreRule.whole`.\n whole: true,\n ...(rule.subjects !== undefined ? { subjects: rule.subjects } : {}),\n };\n}\n\n/**\n * The register as the lines a run prints.\n *\n * Absorption is stated in the *positive* form the operator wrote, because that is\n * the sentence they can check: \"asserts on layout\" is a claim about intent, and\n * \"absorbed 412 token deltas\" is the evidence for or against it.\n */\nexport function summarizeSensitivity(register: SensitivityRegister): readonly string[] {\n if (register.relaxations.length === 0) return [];\n\n const lines = [\n `SENSITIVITY — ${register.totalAbsorbed} difference(s) not asserted on, ` +\n `by ${register.relaxations.length} rule(s)`,\n ];\n\n for (const entry of register.relaxations) {\n if (entry.absorbed === 0) {\n lines.push(\n ` [dead] ${entry.rule} — asserts on ${entry.level} across ` +\n `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason})`,\n );\n continue;\n }\n lines.push(\n ` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +\n `${entry.bands.join('/')} difference(s) in ${entry.subjects.length} subject(s): ` +\n entry.reason,\n );\n }\n\n return lines;\n}\n\n/**\n * Apply declared sensitivities to a run's diffs.\n *\n * A thin fold over {@link applyIgnores}, and thin on purpose: the absorption, the\n * per-rule counting and the dead-rule reporting are one mechanism, and a second\n * implementation of them would be a second set of bugs in the half of the system\n * whose whole job is not to silently hide things.\n *\n * Rules that resolve to `strict` are dropped before the fold rather than passed\n * through, so a project that declares its default explicitly does not collect a\n * permanently-dead register line for saying so.\n */\nexport function applySensitivity(\n diffs: readonly SemanticDiff[],\n rules: readonly SensitivityRule[],\n options: IgnoreOptions = {},\n): SensitivityOutcome {\n const translated = new Map<string, SensitivityRule>();\n const ignores: IgnoreRule[] = [];\n\n for (const rule of rules) {\n const ignore = asIgnore(rule);\n if (ignore === null) continue;\n translated.set(rule.id, rule);\n ignores.push(ignore);\n }\n\n const outcome = applyIgnores(diffs, ignores, options);\n\n const relaxations = [...translated.values()].map((rule) => {\n const absorbed = outcome.register.absorbed.find((entry) => entry.rule === rule.id);\n return {\n rule: rule.id,\n reason: rule.reason,\n level: rule.level,\n absorbed: absorbed?.deltas ?? 0,\n subjects: absorbed?.subjects ?? [],\n bands: absorbed?.bands ?? [],\n };\n });\n\n return {\n diffs: outcome.diffs,\n register: {\n relaxations,\n dead: relaxations.filter((entry) => entry.absorbed === 0).map((entry) => entry.rule),\n totalAbsorbed: relaxations.reduce((sum, entry) => sum + entry.absorbed, 0),\n },\n };\n}\n\n/**\n * What a level does with a set of bands that moved.\n *\n * The arm for the path that holds no second document. `applySensitivity` folds\n * over deltas, which needs both revisions normalized; a `variance run` compares\n * an image against a stored baseline and has only what that baseline carried —\n * per-component hashes, one digest per band (ADR-0027, ADR-0029's split).\n * `bandsBetween` turns those into the same vocabulary, and this decides against\n * it, so the two paths agree by sharing {@link bandsOf} rather than by\n * inspection.\n *\n * **Empty in means nothing absorbed**, not everything. A run whose pixels moved\n * while every component hash held is raster residue — `texture`, which a\n * document cannot carry and which this must therefore never claim to have\n * decided. Absorbing on an empty set would turn the one band a hash comparison\n * is blind to into the one it silences.\n */\nexport function relaxes(\n level: Level,\n moved: readonly Band[],\n): { readonly asserted: readonly Band[]; readonly absorbed: readonly Band[] } {\n const asserting = new Set(bandsOf(level));\n\n return {\n asserted: moved.filter((band) => asserting.has(band)),\n absorbed: moved.filter((band) => !asserting.has(band)),\n };\n}\n\n/**\n * Whether a subject's every moved band falls outside what it is asserted on.\n *\n * The whole subject, because that is the unit a stored baseline can answer for:\n * one image, one verdict. A subject where *some* moved band is asserted on is\n * reported in full — including the bands that would have been absorbed — since\n * a reviewer looking at a nav that moved wants the restyle that came with it,\n * and hiding half a diff is worse than hiding none of it.\n */\nexport function absorbsEntirely(level: Level, moved: readonly Band[]): boolean {\n return moved.length > 0 && relaxes(level, moved).asserted.length === 0;\n}\n"]}
@@ -68,6 +68,7 @@
68
68
  * `core` performs no I/O (ADR-0006); the records this folds are somebody else's
69
69
  * disk.
70
70
  */
71
+ import type { Uses } from './narrow.js';
71
72
  /** Node kinds, in id order. An id is an index into this, never the word. */
72
73
  export declare const NODE_KINDS: readonly ['file', 'component', 'package'];
73
74
  export type NodeKind = (typeof NODE_KINDS)[number];
@@ -176,6 +177,13 @@ export interface Relations {
176
177
  * trail from the change arrives without crossing one of that file's shadows.
177
178
  */
178
179
  readonly shadows: ReadonlyMap<string, readonly string[]>;
180
+ /**
181
+ * How each file uses each file it imports, asked per edge as a walk crosses
182
+ * it ([`narrow.ts`](./narrow.ts)). A lookup rather than a column: the parse
183
+ * that recorded the edge already holds the names, and a walk asks only the
184
+ * edges it reaches. Absent for a graph built without one, which walks whole.
185
+ */
186
+ readonly uses?: Uses;
179
187
  }
180
188
  /**
181
189
  * The lookup key for a node.
@@ -223,5 +231,7 @@ export declare function relationsOf(input: {
223
231
  readonly isolated?: Iterable<Node>;
224
232
  /** Per file, the files its run never reaches; see {@link Relations.shadows}. */
225
233
  readonly shadows?: ReadonlyMap<string, readonly string[]>;
234
+ /** See {@link Relations.uses}. */
235
+ readonly uses?: Uses;
226
236
  }): Relations;
227
237
  //# sourceMappingURL=graph.d.ts.map
@@ -196,7 +196,17 @@ export function relationsOf(input) {
196
196
  if (because !== undefined)
197
197
  reasons.set(id, because);
198
198
  }
199
- return { names, kinds, depends, dependents, unknown, reasons, index, shadows: input.shadows ?? new Map() };
199
+ return {
200
+ names,
201
+ kinds,
202
+ depends,
203
+ dependents,
204
+ unknown,
205
+ reasons,
206
+ index,
207
+ shadows: input.shadows ?? new Map(),
208
+ ...(input.uses === undefined ? {} : { uses: input.uses }),
209
+ };
200
210
  }
201
211
  /**
202
212
  * Edges as ids, sorted and deduplicated.
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/relate/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAU,CAAC;AAGpE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,8CAA8C;IAC9C,SAAS;IACT,+DAA+D;IAC/D,WAAW;IACX,4DAA4D;IAC5D,SAAS;IACT,uEAAuE;IACvE,MAAM;IACN,0EAA0E;IAC1E,OAAO;IACP,gDAAgD;IAChD,aAAa;IACb,8DAA8D;IAC9D,YAAY;IACZ,mFAAmF;IACnF,SAAS;CACD,CAAC;AAGX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAwB,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;AAmF/F;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,KAAK,CAAC,IAAc,EAAE,IAAY;IAChD,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,IAAI,CAAC,SAAoB,EAAE,IAAc,EAAE,IAAY;IACrE,OAAO,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,MAAM,CAAC,SAAoB,EAAE,EAAU;IACrD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAE,CAAE,EAAE,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,SAAoB,EAAE,IAAc;IAC9D,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;QACtD,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAc3B;IACC,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5D,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,EAAE,SAAS,CAAW,CACT,CAAC;IAEtD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgB,CAAC;IACrC,MAAM,GAAG,GAAG,CAAC,IAAU,EAAQ,EAAE;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC;IAEF,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnB,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,YAAY;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACnD,CAAC;IAEF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,KAAK,GAAa,KAAK,CAAC,IAAI,CAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7C,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAC3C,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,YAAY,EAAE,CAAC;QAC3C,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAClD,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QAE/B,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,GAAG,EAAE,EAAE,CAAC;AAC7G,CAAC;AAQD;;;;;;;;GAQG;AACH,SAAS,WAAW,CAClB,SAA8B,EAC9B,KAAkC;IAElC,MAAM,KAAK,GAAW,EAAE,CAAC;IAEzB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,IAAI,IAAI,KAAK,SAAS,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QACrD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAExE,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,MAAM,IAAI,GACR,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;QAClG,IAAI,CAAC,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAsB,EAAE,KAAa,EAAE,EAAiB;IAC3E,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC;IAC3E,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,CAAC;QAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,CAAC,CAAE,GAAG,MAAM,CAAC,EAAE,CAAE,CAAC;IAErF,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAE3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;QACrB,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAE,CAAC;QACxB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACrB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC","sourcesContent":["/**\n * What reaches what, stored so that asking is cheap.\n *\n * Every question this project asks about a change is a reachability question. A\n * diff names files; a subject is made of components; a component is declared in a\n * file that imports other files. *Which subjects could this diff have changed* is\n * one traversal of one graph, and the only reason it is not asked that way today\n * is that there has never been a graph to traverse.\n *\n * ## Why a structure rather than a `Map`\n *\n * The obvious shape is `Map<string, string[]>`, and it is wrong at the size this\n * runs at. A monorepo scan produces tens of thousands of files and several times\n * as many edges; the map holds one array object and one string copy per edge, and\n * a traversal chases a pointer for every step. What replaces it here is the\n * standard answer, and it is standard because nothing has beaten it:\n *\n * - **strings are interned once.** A node is an integer. Comparison is an integer\n * compare, membership is an index into a byte array, and a visited set is a\n * `Uint8Array` rather than a `Set<string>`.\n * - **edges are two typed arrays** in compressed-sparse-row form: `offset` says\n * where a node's row starts, `target` holds the rows end to end. One node's\n * edges are contiguous, so a traversal walks memory forwards instead of\n * following pointers into the heap.\n * - **both directions are materialized.** *What does this depend on* and *what\n * depends on this* are different questions, and a graph answering only the\n * first makes the second a whole-graph scan. The transpose is a counting sort —\n * one pass to count, one to place — so holding it costs `O(n + m)` once instead\n * of `O(n + m)` per query.\n *\n * ## Edges point at what a thing depends on\n *\n * One convention, held everywhere: **`A → B` means A depends on B**, so a change\n * in B may move A. Every affected-set question is therefore a walk against the\n * arrows, which is what `dependentsOf` does, and every *what would I have to read\n * to understand this* question is a walk along them.\n *\n * It decides the direction of the less obvious edges too. A component is\n * `declared-in` the file that declares it — the component depends on the file and\n * not the other way round — so editing that file reaches the component in the\n * same traversal that reaches every importer.\n *\n * ## Nodes are typed, because files are not the last kind\n *\n * Three kinds and the joins between them: a `file` graph built from imports, the\n * `component` nodes the source index already knows how to find, and a `package`\n * for each name an install resolved. That is deliberate rather than incidental. A\n * component relation — *`TodoFooter` renders `Chip`* — is another edge kind\n * between nodes of another kind in this same structure, and\n * [`composition.md`](../../../../docs/composition.md) already computes exactly\n * that relation from what a run rendered. One graph means the static answer and\n * the rendered answer are joinable rather than adjacent.\n *\n * A `package` earns its kind for the same reason and one more: it is the far end\n * of the same line. A run starts at the harness, arrives at our code through the\n * tests, and leaves it again at the packages — so a bumped dependency is a node\n * like any other, and *which of our files a new `jsdom` can affect* is the\n * one traversal this file already performs, seeded on the other side\n * ([`docs/selecting.md`](../../../../docs/selecting.md)).\n *\n * ## What it is not\n *\n * **Not a build graph.** It has no notion of a task, an output or a cache key,\n * and it does not know what an install produced. `nx` and `turbo` own that layer,\n * and what reads them reads their answer rather than competing with it.\n *\n * **Not a resolver.** Nothing here opens a file or knows what a specifier means.\n * `core` performs no I/O (ADR-0006); the records this folds are somebody else's\n * disk.\n */\n\n/** Node kinds, in id order. An id is an index into this, never the word. */\nexport const NODE_KINDS = ['file', 'component', 'package'] as const;\nexport type NodeKind = (typeof NODE_KINDS)[number];\n\n/**\n * Edge kinds, in id order.\n *\n * The distinctions are kept because they explain a finding. One of them also\n * decides a walk: `type` is erased by every compiler, so nothing behind it runs\n * and nothing behind it renders, and the default traversal leaves it out\n * (`RUNTIME_EDGES`). A caller whose question is about source rather than about a\n * runtime — a docgen that reads prop types, a type-check gate — passes\n * `EDGE_KINDS` and walks it.\n */\nexport const EDGE_KINDS = [\n /** `import x from './y'` — a value import. */\n 'imports',\n /** `export … from './y'` — an import that also republishes. */\n 'reexports',\n /** `import('./y')`, with a specifier that was a literal. */\n 'dynamic',\n /** `import type { T } from './y'` — erased before anything renders. */\n 'type',\n /** A stylesheet's `@import`, or a `url()` reaching a font or an image. */\n 'asset',\n /** A component to the file that declares it. */\n 'declared-in',\n /** A package to a package its install resolved beneath it. */\n 'depends-on',\n /** `/// <depends path=\"./y\" />` — a file the module reads without importing it. */\n 'depends',\n] as const;\nexport type EdgeKind = (typeof EDGE_KINDS)[number];\n\n/**\n * The edge kinds a runtime loads through: every kind but `type`. The\n * default for a traversal, and the list a caller widens from. A closure folds a\n * shorter list (`CLOSURE_EDGES`), because a digest is a claim about bytes and\n * one of these kinds leads nowhere any bytes are.\n */\nexport const RUNTIME_EDGES: readonly EdgeKind[] = EDGE_KINDS.filter((kind) => kind !== 'type');\n\nexport interface Node {\n readonly kind: NodeKind;\n /**\n * Repository-relative for a file, the identifier for a component, and for a\n * package the name source code imports it by — `@mui/material`, never a\n * version and never a resolution. Which copy a resolver handed any one\n * importer is unanswerable without running that resolver, and the question\n * this graph is asked never needs it ([`sense/lock`](../../../sense/src/lock/index.ts)).\n */\n readonly name: string;\n}\n\nexport interface Relation {\n readonly from: Node;\n readonly to: Node;\n readonly kind: EdgeKind;\n}\n\n/** An index into `Relations.names`. Valid only against the graph it came from. */\nexport type NodeId = number;\n\n/**\n * One direction of the edge set, compressed-sparse-row.\n *\n * Node `i`'s edges are `target[offset[i] … offset[i + 1])`, and `kind[j]`\n * describes `target[j]`. `offset` holds one more entry than there are nodes, so\n * the last row needs no special case.\n */\nexport interface Adjacency {\n readonly offset: Uint32Array;\n readonly target: Uint32Array;\n readonly kind: Uint8Array;\n}\n\nexport interface Relations {\n /** Node names by id, ordered by kind and then by code unit. */\n readonly names: readonly string[];\n /** The `NODE_KINDS` index of each node. */\n readonly kinds: Uint8Array;\n\n /** What each node depends on. */\n readonly depends: Adjacency;\n /** What depends on each node — the transpose, materialized. */\n readonly dependents: Adjacency;\n\n /**\n * `1` where this node's outgoing edges could not be enumerated.\n *\n * A parse failure, a specifier that was not a literal, a file in a language\n * nothing here reads. Kept for whoever reports on the scan. No walk reads it:\n * the edge a reader could not see is one a recorded run sees load.\n */\n readonly unknown: Uint8Array;\n\n /**\n * Why, for each of those nodes, in the words whoever read the file used.\n *\n * Sparse — a repository the scan understands carries none of these — and kept\n * apart from the mask because the mask is what the inner loop tests and this is\n * what an operator reads. Without it, a run that observed forty subjects instead\n * of twelve can say *because eight files could not be read* and can never say\n * **which**, which is the difference between a fact and a work item.\n */\n readonly reasons: ReadonlyMap<NodeId, string>;\n\n /** `keyOf` to id — the interning table, kept for lookup. */\n readonly index: ReadonlyMap<string, NodeId>;\n\n /**\n * Per file, the files its run never reaches at any depth: what a mock\n * replaces for the whole of a test's run.\n *\n * Carried with the graph rather than handed to each walk, because the graph\n * crosses every seam a walk does and a table left behind at one of them is a\n * selection that quietly widened. Empty for a graph nobody tainted. A walk\n * against the arrows consults it: a change affects a file only when some\n * trail from the change arrives without crossing one of that file's shadows.\n */\n readonly shadows: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * The lookup key for a node.\n *\n * Runtime only; nothing serializes this, and no code parses it back — the\n * separator only has to be injective, and it is, because a kind is one of a few\n * fixed words and none contains a `:`. A name that contains one is therefore\n * fine.\n *\n * The obvious separator is a NUL, and it is the wrong one: a source file\n * carrying that byte stops being text to `grep`, `file` and `git diff`, which is\n * a search finding nothing looking exactly like a search that returned nothing.\n * `tools/boundaries.check.ts` fails a build over it.\n */\nexport function keyOf(kind: NodeKind, name: string): string {\n return `${kind}:${name}`;\n}\n\n/** The id a node is known by in these relations, or `undefined` for a name the scan never met. */\nexport function idOf(relations: Relations, kind: NodeKind, name: string): NodeId | undefined {\n return relations.index.get(keyOf(kind, name));\n}\n\n/** The node an id names: its kind and name, or `undefined` past the end. */\nexport function nodeAt(relations: Relations, id: NodeId): Node | undefined {\n const name = relations.names[id];\n if (name === undefined) return undefined;\n return { kind: NODE_KINDS[relations.kinds[id]!]!, name };\n}\n\n/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */\nexport function nodesOfKind(relations: Relations, kind: NodeKind): readonly NodeId[] {\n const wanted = NODE_KINDS.indexOf(kind);\n const found: NodeId[] = [];\n for (let id = 0; id < relations.kinds.length; id += 1) {\n if (relations.kinds[id] === wanted) found.push(id);\n }\n return found;\n}\n\n/**\n * Fold relations into the queryable form.\n *\n * Deterministic to the byte: nodes are ordered by kind and then by **code unit** —\n * never `localeCompare`, which would make an id a promise about `LANG` — and edges\n * are sorted and deduplicated, so two runs over the same records produce identical\n * arrays and a serialized graph is stable.\n *\n * `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The\n * transpose is a counting sort rather than a second pass over a map, which is why\n * both directions cost one build instead of one per query.\n */\nexport function relationsOf(input: {\n readonly relations: Iterable<Relation>;\n /**\n * Nodes whose outgoing edges are not fully known, and must be treated as such.\n *\n * A bare node says *this one is blind*. A pair says why, in whatever words the\n * reader of the file used — and the sentence is the half an operator can act on,\n * so it travels with the fact rather than being reconstructed from it.\n */\n readonly unknown?: Iterable<Node | readonly [Node, string]>;\n /** Nodes with no edges at all, which would otherwise be absent from the graph. */\n readonly isolated?: Iterable<Node>;\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}): Relations {\n const relations = [...input.relations];\n const unknownNodes = [...(input.unknown ?? [])].map((entry) =>\n Array.isArray(entry) ? entry : ([entry, undefined] as const),\n ) as readonly (readonly [Node, string | undefined])[];\n\n const seen = new Map<string, Node>();\n const see = (node: Node): void => {\n const key = keyOf(node.kind, node.name);\n if (!seen.has(key)) seen.set(key, node);\n };\n\n for (const relation of relations) {\n see(relation.from);\n see(relation.to);\n }\n for (const [node] of unknownNodes) see(node);\n for (const node of input.isolated ?? []) see(node);\n\n // Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,\n // which is what lets a caller filter to files without touching a string.\n const ordered = [...seen.values()].sort(\n (a, b) =>\n NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||\n (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n\n const index = new Map<string, NodeId>();\n const names: string[] = Array.from<string>({ length: ordered.length });\n const kinds = new Uint8Array(ordered.length);\n\n for (const [id, node] of ordered.entries()) {\n index.set(keyOf(node.kind, node.name), id);\n names[id] = node.name;\n kinds[id] = NODE_KINDS.indexOf(node.kind);\n }\n\n const edges = sortedEdges(relations, index);\n const depends = adjacencyOf(edges, ordered.length, 'from');\n const dependents = adjacencyOf(edges, ordered.length, 'to');\n\n const unknown = new Uint8Array(ordered.length);\n const reasons = new Map<NodeId, string>();\n for (const [node, because] of unknownNodes) {\n const id = index.get(keyOf(node.kind, node.name));\n if (id === undefined) continue;\n\n unknown[id] = 1;\n if (because !== undefined) reasons.set(id, because);\n }\n\n return { names, kinds, depends, dependents, unknown, reasons, index, shadows: input.shadows ?? new Map() };\n}\n\ninterface Edge {\n readonly from: NodeId;\n readonly to: NodeId;\n readonly kind: number;\n}\n\n/**\n * Edges as ids, sorted and deduplicated.\n *\n * Sorting the whole list once is what makes every row of *both* adjacencies\n * sorted without sorting any row: each fill walks the list in order, so a row\n * receives its targets in order. Two files related twice — a value import and a\n * type import — stay two edges, because the kinds differ and one of them explains\n * a finding the other does not.\n */\nfunction sortedEdges(\n relations: readonly Relation[],\n index: ReadonlyMap<string, NodeId>,\n): readonly Edge[] {\n const edges: Edge[] = [];\n\n for (const relation of relations) {\n const from = index.get(keyOf(relation.from.kind, relation.from.name));\n const to = index.get(keyOf(relation.to.kind, relation.to.name));\n if (from === undefined || to === undefined) continue;\n edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });\n }\n\n edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);\n\n const unique: Edge[] = [];\n for (const edge of edges) {\n const last = unique[unique.length - 1];\n const same =\n last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;\n if (!same) unique.push(edge);\n }\n\n return unique;\n}\n\n/**\n * One direction, by counting sort.\n *\n * `by` names the endpoint a row is keyed on: `from` produces *what this depends\n * on*, `to` produces the transpose. Two passes — count the degrees, then place —\n * and no per-node array is ever allocated.\n */\nfunction adjacencyOf(edges: readonly Edge[], nodes: number, by: 'from' | 'to'): Adjacency {\n const offset = new Uint32Array(nodes + 1);\n for (const edge of edges) offset[edge[by] + 1] = offset[edge[by] + 1]! + 1;\n for (let id = 0; id < nodes; id += 1) offset[id + 1] = offset[id + 1]! + offset[id]!;\n\n const target = new Uint32Array(edges.length);\n const kind = new Uint8Array(edges.length);\n const cursor = Uint32Array.from(offset.subarray(0, nodes));\n\n for (const edge of edges) {\n const row = edge[by];\n const at = cursor[row]!;\n target[at] = by === 'from' ? edge.to : edge.from;\n kind[at] = edge.kind;\n cursor[row] = at + 1;\n }\n\n return { offset, target, kind };\n}\n"]}
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/relate/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What reaches what, stored so that asking is cheap.\n *\n * Every question this project asks about a change is a reachability question. A\n * diff names files; a subject is made of components; a component is declared in a\n * file that imports other files. *Which subjects could this diff have changed* is\n * one traversal of one graph, and the only reason it is not asked that way today\n * is that there has never been a graph to traverse.\n *\n * ## Why a structure rather than a `Map`\n *\n * The obvious shape is `Map<string, string[]>`, and it is wrong at the size this\n * runs at. A monorepo scan produces tens of thousands of files and several times\n * as many edges; the map holds one array object and one string copy per edge, and\n * a traversal chases a pointer for every step. What replaces it here is the\n * standard answer, and it is standard because nothing has beaten it:\n *\n * - **strings are interned once.** A node is an integer. Comparison is an integer\n * compare, membership is an index into a byte array, and a visited set is a\n * `Uint8Array` rather than a `Set<string>`.\n * - **edges are two typed arrays** in compressed-sparse-row form: `offset` says\n * where a node's row starts, `target` holds the rows end to end. One node's\n * edges are contiguous, so a traversal walks memory forwards instead of\n * following pointers into the heap.\n * - **both directions are materialized.** *What does this depend on* and *what\n * depends on this* are different questions, and a graph answering only the\n * first makes the second a whole-graph scan. The transpose is a counting sort —\n * one pass to count, one to place — so holding it costs `O(n + m)` once instead\n * of `O(n + m)` per query.\n *\n * ## Edges point at what a thing depends on\n *\n * One convention, held everywhere: **`A → B` means A depends on B**, so a change\n * in B may move A. Every affected-set question is therefore a walk against the\n * arrows, which is what `dependentsOf` does, and every *what would I have to read\n * to understand this* question is a walk along them.\n *\n * It decides the direction of the less obvious edges too. A component is\n * `declared-in` the file that declares it — the component depends on the file and\n * not the other way round — so editing that file reaches the component in the\n * same traversal that reaches every importer.\n *\n * ## Nodes are typed, because files are not the last kind\n *\n * Three kinds and the joins between them: a `file` graph built from imports, the\n * `component` nodes the source index already knows how to find, and a `package`\n * for each name an install resolved. That is deliberate rather than incidental. A\n * component relation — *`TodoFooter` renders `Chip`* — is another edge kind\n * between nodes of another kind in this same structure, and\n * [`composition.md`](../../../../docs/composition.md) already computes exactly\n * that relation from what a run rendered. One graph means the static answer and\n * the rendered answer are joinable rather than adjacent.\n *\n * A `package` earns its kind for the same reason and one more: it is the far end\n * of the same line. A run starts at the harness, arrives at our code through the\n * tests, and leaves it again at the packages — so a bumped dependency is a node\n * like any other, and *which of our files a new `jsdom` can affect* is the\n * one traversal this file already performs, seeded on the other side\n * ([`docs/selecting.md`](../../../../docs/selecting.md)).\n *\n * ## What it is not\n *\n * **Not a build graph.** It has no notion of a task, an output or a cache key,\n * and it does not know what an install produced. `nx` and `turbo` own that layer,\n * and what reads them reads their answer rather than competing with it.\n *\n * **Not a resolver.** Nothing here opens a file or knows what a specifier means.\n * `core` performs no I/O (ADR-0006); the records this folds are somebody else's\n * disk.\n */\n\nimport type { Uses } from './narrow.js';\n\n/** Node kinds, in id order. An id is an index into this, never the word. */\nexport const NODE_KINDS = ['file', 'component', 'package'] as const;\nexport type NodeKind = (typeof NODE_KINDS)[number];\n\n/**\n * Edge kinds, in id order.\n *\n * The distinctions are kept because they explain a finding. One of them also\n * decides a walk: `type` is erased by every compiler, so nothing behind it runs\n * and nothing behind it renders, and the default traversal leaves it out\n * (`RUNTIME_EDGES`). A caller whose question is about source rather than about a\n * runtime — a docgen that reads prop types, a type-check gate — passes\n * `EDGE_KINDS` and walks it.\n */\nexport const EDGE_KINDS = [\n /** `import x from './y'` — a value import. */\n 'imports',\n /** `export … from './y'` — an import that also republishes. */\n 'reexports',\n /** `import('./y')`, with a specifier that was a literal. */\n 'dynamic',\n /** `import type { T } from './y'` — erased before anything renders. */\n 'type',\n /** A stylesheet's `@import`, or a `url()` reaching a font or an image. */\n 'asset',\n /** A component to the file that declares it. */\n 'declared-in',\n /** A package to a package its install resolved beneath it. */\n 'depends-on',\n /** `/// <depends path=\"./y\" />` — a file the module reads without importing it. */\n 'depends',\n] as const;\nexport type EdgeKind = (typeof EDGE_KINDS)[number];\n\n/**\n * The edge kinds a runtime loads through: every kind but `type`. The\n * default for a traversal, and the list a caller widens from. A closure folds a\n * shorter list (`CLOSURE_EDGES`), because a digest is a claim about bytes and\n * one of these kinds leads nowhere any bytes are.\n */\nexport const RUNTIME_EDGES: readonly EdgeKind[] = EDGE_KINDS.filter((kind) => kind !== 'type');\n\nexport interface Node {\n readonly kind: NodeKind;\n /**\n * Repository-relative for a file, the identifier for a component, and for a\n * package the name source code imports it by — `@mui/material`, never a\n * version and never a resolution. Which copy a resolver handed any one\n * importer is unanswerable without running that resolver, and the question\n * this graph is asked never needs it ([`sense/lock`](../../../sense/src/lock/index.ts)).\n */\n readonly name: string;\n}\n\nexport interface Relation {\n readonly from: Node;\n readonly to: Node;\n readonly kind: EdgeKind;\n}\n\n/** An index into `Relations.names`. Valid only against the graph it came from. */\nexport type NodeId = number;\n\n/**\n * One direction of the edge set, compressed-sparse-row.\n *\n * Node `i`'s edges are `target[offset[i] … offset[i + 1])`, and `kind[j]`\n * describes `target[j]`. `offset` holds one more entry than there are nodes, so\n * the last row needs no special case.\n */\nexport interface Adjacency {\n readonly offset: Uint32Array;\n readonly target: Uint32Array;\n readonly kind: Uint8Array;\n}\n\nexport interface Relations {\n /** Node names by id, ordered by kind and then by code unit. */\n readonly names: readonly string[];\n /** The `NODE_KINDS` index of each node. */\n readonly kinds: Uint8Array;\n\n /** What each node depends on. */\n readonly depends: Adjacency;\n /** What depends on each node — the transpose, materialized. */\n readonly dependents: Adjacency;\n\n /**\n * `1` where this node's outgoing edges could not be enumerated.\n *\n * A parse failure, a specifier that was not a literal, a file in a language\n * nothing here reads. Kept for whoever reports on the scan. No walk reads it:\n * the edge a reader could not see is one a recorded run sees load.\n */\n readonly unknown: Uint8Array;\n\n /**\n * Why, for each of those nodes, in the words whoever read the file used.\n *\n * Sparse — a repository the scan understands carries none of these — and kept\n * apart from the mask because the mask is what the inner loop tests and this is\n * what an operator reads. Without it, a run that observed forty subjects instead\n * of twelve can say *because eight files could not be read* and can never say\n * **which**, which is the difference between a fact and a work item.\n */\n readonly reasons: ReadonlyMap<NodeId, string>;\n\n /** `keyOf` to id — the interning table, kept for lookup. */\n readonly index: ReadonlyMap<string, NodeId>;\n\n /**\n * Per file, the files its run never reaches at any depth: what a mock\n * replaces for the whole of a test's run.\n *\n * Carried with the graph rather than handed to each walk, because the graph\n * crosses every seam a walk does and a table left behind at one of them is a\n * selection that quietly widened. Empty for a graph nobody tainted. A walk\n * against the arrows consults it: a change affects a file only when some\n * trail from the change arrives without crossing one of that file's shadows.\n */\n readonly shadows: ReadonlyMap<string, readonly string[]>;\n\n /**\n * How each file uses each file it imports, asked per edge as a walk crosses\n * it ([`narrow.ts`](./narrow.ts)). A lookup rather than a column: the parse\n * that recorded the edge already holds the names, and a walk asks only the\n * edges it reaches. Absent for a graph built without one, which walks whole.\n */\n readonly uses?: Uses;\n}\n\n/**\n * The lookup key for a node.\n *\n * Runtime only; nothing serializes this, and no code parses it back — the\n * separator only has to be injective, and it is, because a kind is one of a few\n * fixed words and none contains a `:`. A name that contains one is therefore\n * fine.\n *\n * The obvious separator is a NUL, and it is the wrong one: a source file\n * carrying that byte stops being text to `grep`, `file` and `git diff`, which is\n * a search finding nothing looking exactly like a search that returned nothing.\n * `tools/boundaries.check.ts` fails a build over it.\n */\nexport function keyOf(kind: NodeKind, name: string): string {\n return `${kind}:${name}`;\n}\n\n/** The id a node is known by in these relations, or `undefined` for a name the scan never met. */\nexport function idOf(relations: Relations, kind: NodeKind, name: string): NodeId | undefined {\n return relations.index.get(keyOf(kind, name));\n}\n\n/** The node an id names: its kind and name, or `undefined` past the end. */\nexport function nodeAt(relations: Relations, id: NodeId): Node | undefined {\n const name = relations.names[id];\n if (name === undefined) return undefined;\n return { kind: NODE_KINDS[relations.kinds[id]!]!, name };\n}\n\n/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */\nexport function nodesOfKind(relations: Relations, kind: NodeKind): readonly NodeId[] {\n const wanted = NODE_KINDS.indexOf(kind);\n const found: NodeId[] = [];\n for (let id = 0; id < relations.kinds.length; id += 1) {\n if (relations.kinds[id] === wanted) found.push(id);\n }\n return found;\n}\n\n/**\n * Fold relations into the queryable form.\n *\n * Deterministic to the byte: nodes are ordered by kind and then by **code unit** —\n * never `localeCompare`, which would make an id a promise about `LANG` — and edges\n * are sorted and deduplicated, so two runs over the same records produce identical\n * arrays and a serialized graph is stable.\n *\n * `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The\n * transpose is a counting sort rather than a second pass over a map, which is why\n * both directions cost one build instead of one per query.\n */\nexport function relationsOf(input: {\n readonly relations: Iterable<Relation>;\n /**\n * Nodes whose outgoing edges are not fully known, and must be treated as such.\n *\n * A bare node says *this one is blind*. A pair says why, in whatever words the\n * reader of the file used — and the sentence is the half an operator can act on,\n * so it travels with the fact rather than being reconstructed from it.\n */\n readonly unknown?: Iterable<Node | readonly [Node, string]>;\n /** Nodes with no edges at all, which would otherwise be absent from the graph. */\n readonly isolated?: Iterable<Node>;\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n /** See {@link Relations.uses}. */\n readonly uses?: Uses;\n}): Relations {\n const relations = [...input.relations];\n const unknownNodes = [...(input.unknown ?? [])].map((entry) =>\n Array.isArray(entry) ? entry : ([entry, undefined] as const),\n ) as readonly (readonly [Node, string | undefined])[];\n\n const seen = new Map<string, Node>();\n const see = (node: Node): void => {\n const key = keyOf(node.kind, node.name);\n if (!seen.has(key)) seen.set(key, node);\n };\n\n for (const relation of relations) {\n see(relation.from);\n see(relation.to);\n }\n for (const [node] of unknownNodes) see(node);\n for (const node of input.isolated ?? []) see(node);\n\n // Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,\n // which is what lets a caller filter to files without touching a string.\n const ordered = [...seen.values()].sort(\n (a, b) =>\n NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||\n (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n\n const index = new Map<string, NodeId>();\n const names: string[] = Array.from<string>({ length: ordered.length });\n const kinds = new Uint8Array(ordered.length);\n\n for (const [id, node] of ordered.entries()) {\n index.set(keyOf(node.kind, node.name), id);\n names[id] = node.name;\n kinds[id] = NODE_KINDS.indexOf(node.kind);\n }\n\n const edges = sortedEdges(relations, index);\n const depends = adjacencyOf(edges, ordered.length, 'from');\n const dependents = adjacencyOf(edges, ordered.length, 'to');\n\n const unknown = new Uint8Array(ordered.length);\n const reasons = new Map<NodeId, string>();\n for (const [node, because] of unknownNodes) {\n const id = index.get(keyOf(node.kind, node.name));\n if (id === undefined) continue;\n\n unknown[id] = 1;\n if (because !== undefined) reasons.set(id, because);\n }\n\n return {\n names,\n kinds,\n depends,\n dependents,\n unknown,\n reasons,\n index,\n shadows: input.shadows ?? new Map(),\n ...(input.uses === undefined ? {} : { uses: input.uses }),\n };\n}\n\ninterface Edge {\n readonly from: NodeId;\n readonly to: NodeId;\n readonly kind: number;\n}\n\n/**\n * Edges as ids, sorted and deduplicated.\n *\n * Sorting the whole list once is what makes every row of *both* adjacencies\n * sorted without sorting any row: each fill walks the list in order, so a row\n * receives its targets in order. Two files related twice — a value import and a\n * type import — stay two edges, because the kinds differ and one of them explains\n * a finding the other does not.\n */\nfunction sortedEdges(\n relations: readonly Relation[],\n index: ReadonlyMap<string, NodeId>,\n): readonly Edge[] {\n const edges: Edge[] = [];\n\n for (const relation of relations) {\n const from = index.get(keyOf(relation.from.kind, relation.from.name));\n const to = index.get(keyOf(relation.to.kind, relation.to.name));\n if (from === undefined || to === undefined) continue;\n edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });\n }\n\n edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);\n\n const unique: Edge[] = [];\n for (const edge of edges) {\n const last = unique[unique.length - 1];\n const same =\n last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;\n if (!same) unique.push(edge);\n }\n\n return unique;\n}\n\n/**\n * One direction, by counting sort.\n *\n * `by` names the endpoint a row is keyed on: `from` produces *what this depends\n * on*, `to` produces the transpose. Two passes — count the degrees, then place —\n * and no per-node array is ever allocated.\n */\nfunction adjacencyOf(edges: readonly Edge[], nodes: number, by: 'from' | 'to'): Adjacency {\n const offset = new Uint32Array(nodes + 1);\n for (const edge of edges) offset[edge[by] + 1] = offset[edge[by] + 1]! + 1;\n for (let id = 0; id < nodes; id += 1) offset[id + 1] = offset[id + 1]! + offset[id]!;\n\n const target = new Uint32Array(edges.length);\n const kind = new Uint8Array(edges.length);\n const cursor = Uint32Array.from(offset.subarray(0, nodes));\n\n for (const edge of edges) {\n const row = edge[by];\n const at = cursor[row]!;\n target[at] = by === 'from' ? edge.to : edge.from;\n kind[at] = edge.kind;\n cursor[row] = at + 1;\n }\n\n return { offset, target, kind };\n}\n"]}
@@ -11,6 +11,7 @@
11
11
  */
12
12
  export { EDGE_KINDS, NODE_KINDS, RUNTIME_EDGES, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, type Adjacency, type EdgeKind, type Node, type NodeId, type NodeKind, type Relation, type Relations, } from './graph.js';
13
13
  export { beforeReach, changedBefore, within, type BeforeReach, type BeforeReachOptions, } from './before.js';
14
+ export { type EdgeUse, type Uses } from './narrow.js';
14
15
  export { dependenciesOf, dependentsOf, trailOf, type Traversal, type TraversalOptions, } from './reach.js';
15
16
  export { CLOSURE_EDGES, closureOf, driftedBetween, type Closure, type ClosureInput, type Drift, } from './merkle.js';
16
17
  export { explain, affectedBy, relationsOfFiles, type FileEdge, type FileRecord, type AffectedOptions, type PackageEdge, type RelationsOptions, type Affected, } from './records.js';
@@ -11,6 +11,7 @@
11
11
  */
12
12
  export { EDGE_KINDS, NODE_KINDS, RUNTIME_EDGES, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, } from './graph.js';
13
13
  export { beforeReach, changedBefore, within, } from './before.js';
14
+ export {} from './narrow.js';
14
15
  export { dependenciesOf, dependentsOf, trailOf, } from './reach.js';
15
16
  export { CLOSURE_EDGES, closureOf, driftedBetween, } from './merkle.js';
16
17
  export { explain, affectedBy, relationsOfFiles, } from './records.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/relate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,aAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,WAAW,GAQZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,WAAW,EACX,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,GAGR,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,GAIf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,OAAO,EACP,UAAU,EACV,gBAAgB,GAOjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * `core/relate` — what reaches what.\n *\n * A typed, interned, bidirectional graph and the two traversals over it, plus the\n * file-shaped record the scanner produces and the fold that turns a pile of them\n * into the structure. No I/O, no parser, no resolver: the expensive half belongs\n * to whoever owns the disk, and this is the half that has to be fast and has to\n * be provable without one.\n *\n * See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.\n */\n\nexport {\n EDGE_KINDS,\n NODE_KINDS,\n RUNTIME_EDGES,\n idOf,\n keyOf,\n nodeAt,\n nodesOfKind,\n relationsOf,\n type Adjacency,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\n\nexport {\n beforeReach,\n changedBefore,\n within,\n type BeforeReach,\n type BeforeReachOptions,\n} from './before.js';\n\nexport {\n dependenciesOf,\n dependentsOf,\n trailOf,\n type Traversal,\n type TraversalOptions,\n} from './reach.js';\n\nexport {\n CLOSURE_EDGES,\n closureOf,\n driftedBetween,\n type Closure,\n type ClosureInput,\n type Drift,\n} from './merkle.js';\n\nexport {\n explain,\n affectedBy,\n relationsOfFiles,\n type FileEdge,\n type FileRecord,\n type AffectedOptions,\n type PackageEdge,\n type RelationsOptions,\n type Affected,\n} from './records.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/relate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,aAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,WAAW,GAQZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,WAAW,EACX,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AAErB,OAAO,EAA2B,MAAM,aAAa,CAAC;AAEtD,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,GAGR,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,GAIf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,OAAO,EACP,UAAU,EACV,gBAAgB,GAOjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * `core/relate` — what reaches what.\n *\n * A typed, interned, bidirectional graph and the two traversals over it, plus the\n * file-shaped record the scanner produces and the fold that turns a pile of them\n * into the structure. No I/O, no parser, no resolver: the expensive half belongs\n * to whoever owns the disk, and this is the half that has to be fast and has to\n * be provable without one.\n *\n * See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.\n */\n\nexport {\n EDGE_KINDS,\n NODE_KINDS,\n RUNTIME_EDGES,\n idOf,\n keyOf,\n nodeAt,\n nodesOfKind,\n relationsOf,\n type Adjacency,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\n\nexport {\n beforeReach,\n changedBefore,\n within,\n type BeforeReach,\n type BeforeReachOptions,\n} from './before.js';\n\nexport { type EdgeUse, type Uses } from './narrow.js';\n\nexport {\n dependenciesOf,\n dependentsOf,\n trailOf,\n type Traversal,\n type TraversalOptions,\n} from './reach.js';\n\nexport {\n CLOSURE_EDGES,\n closureOf,\n driftedBetween,\n type Closure,\n type ClosureInput,\n type Drift,\n} from './merkle.js';\n\nexport {\n explain,\n affectedBy,\n relationsOfFiles,\n type FileEdge,\n type FileRecord,\n type AffectedOptions,\n type PackageEdge,\n type RelationsOptions,\n type Affected,\n} from './records.js';\n"]}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The walk against the arrows, narrowed by the names each importer uses.
3
+ *
4
+ * A file-graph edge says *this file imports that one*, and a plain walk charges
5
+ * every importer for every change. An importer reaches a module only through
6
+ * the names it binds, though, so a change that moved `total` and left `label`
7
+ * alone reaches the files that import `total` and not the ones that import
8
+ * `label`. That is tree shaking, asked of reach instead of a bundle.
9
+ *
10
+ * Each visited file carries one of two states:
11
+ *
12
+ * - **whole** — what it does as it loads moved, or nobody said which of its
13
+ * exports did. Every dependent is entered, as the plain walk does.
14
+ * - **the exports that moved** — read from the file's two texts for a seed,
15
+ * or carried through a re-export for a barrel. A dependent that imports one
16
+ * of them is entered whole: which of *its* exports read the name is a
17
+ * question about its body, and nothing here reads one. A dependent that
18
+ * re-exports one is entered carrying the names it republishes them under.
19
+ *
20
+ * Every answer the use cannot give is whole: an edge the lookup does not
21
+ * know, a namespace import, a `require`, an `import()`, a stylesheet, a
22
+ * component declared in the file. A narrower walk has to be earned by a name
23
+ * the parse recorded, so an unknown can only cost a wider one.
24
+ *
25
+ * A file's names only grow, and a file that grew is expanded again, so the walk
26
+ * reaches the same fixpoint whatever order the seeds arrive in.
27
+ */
28
+ import { type Adjacency, type NodeId } from './graph.js';
29
+ import type { Traversal } from './reach.js';
30
+ /** How one file uses one file it imports, read from the parse that recorded the edge. */
31
+ export interface EdgeUse {
32
+ /**
33
+ * The names an `import` binds from the target, `default` for a default
34
+ * import. Absent when the use is the whole module: a namespace, a `require`,
35
+ * an `import` that binds nothing, which a `require` is stored as.
36
+ */
37
+ readonly imports?: readonly string[];
38
+ /**
39
+ * What a re-export republishes, as `[the target's name, this file's name]`.
40
+ * `['*', '*']` is `export * from`, which republishes every name but
41
+ * `default`; `['*', 'ns']` is `export * as ns from`. Absent when the parse
42
+ * did not record what the file publishes.
43
+ */
44
+ readonly reexports?: readonly (readonly [string, string])[];
45
+ }
46
+ /** How `importer` uses `target`, both by file path; `undefined` is whole. */
47
+ export type Uses = (importer: string, target: string) => EdgeUse | undefined;
48
+ /**
49
+ * Breadth-first against the arrows from `seeds`, each seed in `moved` carrying
50
+ * the exports its change moved and every other one whole.
51
+ */
52
+ export declare function narrowed(dependents: Adjacency, names: readonly string[], seeds: Iterable<NodeId>, moved: ReadonlyMap<NodeId, readonly string[]>, uses: Uses, allowed: Uint8Array, avoid?: Iterable<NodeId>): Traversal;
53
+ //# sourceMappingURL=narrow.d.ts.map