@amalgm/live 0.1.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 (159) hide show
  1. package/PURPOSE.md +103 -0
  2. package/README.md +45 -0
  3. package/dist/contracts/binding.d.ts +49 -0
  4. package/dist/contracts/binding.d.ts.map +1 -0
  5. package/dist/contracts/binding.js +23 -0
  6. package/dist/contracts/binding.js.map +1 -0
  7. package/dist/contracts/content.d.ts +42 -0
  8. package/dist/contracts/content.d.ts.map +1 -0
  9. package/dist/contracts/content.js +72 -0
  10. package/dist/contracts/content.js.map +1 -0
  11. package/dist/contracts/docs.d.ts +64 -0
  12. package/dist/contracts/docs.d.ts.map +1 -0
  13. package/dist/contracts/docs.js +51 -0
  14. package/dist/contracts/docs.js.map +1 -0
  15. package/dist/contracts/events.d.ts +45 -0
  16. package/dist/contracts/events.d.ts.map +1 -0
  17. package/dist/contracts/events.js +38 -0
  18. package/dist/contracts/events.js.map +1 -0
  19. package/dist/contracts/frames/gateway.d.ts +83 -0
  20. package/dist/contracts/frames/gateway.d.ts.map +1 -0
  21. package/dist/contracts/frames/gateway.js +35 -0
  22. package/dist/contracts/frames/gateway.js.map +1 -0
  23. package/dist/contracts/frames/stream.d.ts +59 -0
  24. package/dist/contracts/frames/stream.d.ts.map +1 -0
  25. package/dist/contracts/frames/stream.js +17 -0
  26. package/dist/contracts/frames/stream.js.map +1 -0
  27. package/dist/contracts/frames/tunnel.d.ts +78 -0
  28. package/dist/contracts/frames/tunnel.d.ts.map +1 -0
  29. package/dist/contracts/frames/tunnel.js +55 -0
  30. package/dist/contracts/frames/tunnel.js.map +1 -0
  31. package/dist/contracts/index.d.ts +19 -0
  32. package/dist/contracts/index.d.ts.map +1 -0
  33. package/dist/contracts/index.js +19 -0
  34. package/dist/contracts/index.js.map +1 -0
  35. package/dist/contracts/mutation.d.ts +51 -0
  36. package/dist/contracts/mutation.d.ts.map +1 -0
  37. package/dist/contracts/mutation.js +55 -0
  38. package/dist/contracts/mutation.js.map +1 -0
  39. package/dist/contracts/presence.d.ts +46 -0
  40. package/dist/contracts/presence.d.ts.map +1 -0
  41. package/dist/contracts/presence.js +34 -0
  42. package/dist/contracts/presence.js.map +1 -0
  43. package/dist/contracts/resources.d.ts +52 -0
  44. package/dist/contracts/resources.d.ts.map +1 -0
  45. package/dist/contracts/resources.js +105 -0
  46. package/dist/contracts/resources.js.map +1 -0
  47. package/dist/contracts/snapshot.d.ts +24 -0
  48. package/dist/contracts/snapshot.d.ts.map +1 -0
  49. package/dist/contracts/snapshot.js +28 -0
  50. package/dist/contracts/snapshot.js.map +1 -0
  51. package/dist/contracts/validate.d.ts +32 -0
  52. package/dist/contracts/validate.d.ts.map +1 -0
  53. package/dist/contracts/validate.js +25 -0
  54. package/dist/contracts/validate.js.map +1 -0
  55. package/dist/detection/adapter-contract.d.ts +134 -0
  56. package/dist/detection/adapter-contract.d.ts.map +1 -0
  57. package/dist/detection/adapter-contract.js +162 -0
  58. package/dist/detection/adapter-contract.js.map +1 -0
  59. package/dist/detection/classify.d.ts +31 -0
  60. package/dist/detection/classify.d.ts.map +1 -0
  61. package/dist/detection/classify.js +28 -0
  62. package/dist/detection/classify.js.map +1 -0
  63. package/dist/detection/continuity.d.ts +54 -0
  64. package/dist/detection/continuity.d.ts.map +1 -0
  65. package/dist/detection/continuity.js +85 -0
  66. package/dist/detection/continuity.js.map +1 -0
  67. package/dist/detection/coverage.d.ts +43 -0
  68. package/dist/detection/coverage.d.ts.map +1 -0
  69. package/dist/detection/coverage.js +60 -0
  70. package/dist/detection/coverage.js.map +1 -0
  71. package/dist/detection/diff.d.ts +144 -0
  72. package/dist/detection/diff.d.ts.map +1 -0
  73. package/dist/detection/diff.js +187 -0
  74. package/dist/detection/diff.js.map +1 -0
  75. package/dist/detection/enrollment.d.ts +16 -0
  76. package/dist/detection/enrollment.d.ts.map +1 -0
  77. package/dist/detection/enrollment.js +70 -0
  78. package/dist/detection/enrollment.js.map +1 -0
  79. package/dist/detection/index.d.ts +29 -0
  80. package/dist/detection/index.d.ts.map +1 -0
  81. package/dist/detection/index.js +29 -0
  82. package/dist/detection/index.js.map +1 -0
  83. package/dist/detection/refusal.d.ts +33 -0
  84. package/dist/detection/refusal.d.ts.map +1 -0
  85. package/dist/detection/refusal.js +40 -0
  86. package/dist/detection/refusal.js.map +1 -0
  87. package/dist/entities/bytes.d.ts +18 -0
  88. package/dist/entities/bytes.d.ts.map +1 -0
  89. package/dist/entities/bytes.js +61 -0
  90. package/dist/entities/bytes.js.map +1 -0
  91. package/dist/entities/cloud.d.ts +82 -0
  92. package/dist/entities/cloud.d.ts.map +1 -0
  93. package/dist/entities/cloud.js +136 -0
  94. package/dist/entities/cloud.js.map +1 -0
  95. package/dist/entities/index.d.ts +23 -0
  96. package/dist/entities/index.d.ts.map +1 -0
  97. package/dist/entities/index.js +23 -0
  98. package/dist/entities/index.js.map +1 -0
  99. package/dist/entities/moves.d.ts +42 -0
  100. package/dist/entities/moves.d.ts.map +1 -0
  101. package/dist/entities/moves.js +82 -0
  102. package/dist/entities/moves.js.map +1 -0
  103. package/dist/entities/repo-states.d.ts +121 -0
  104. package/dist/entities/repo-states.d.ts.map +1 -0
  105. package/dist/entities/repo-states.js +127 -0
  106. package/dist/entities/repo-states.js.map +1 -0
  107. package/dist/entities/rules.d.ts +104 -0
  108. package/dist/entities/rules.d.ts.map +1 -0
  109. package/dist/entities/rules.js +200 -0
  110. package/dist/entities/rules.js.map +1 -0
  111. package/dist/entities/types.d.ts +94 -0
  112. package/dist/entities/types.d.ts.map +1 -0
  113. package/dist/entities/types.js +40 -0
  114. package/dist/entities/types.js.map +1 -0
  115. package/dist/index.d.ts +17 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +17 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/machines/arbitration.d.ts +36 -0
  120. package/dist/machines/arbitration.d.ts.map +1 -0
  121. package/dist/machines/arbitration.js +37 -0
  122. package/dist/machines/arbitration.js.map +1 -0
  123. package/dist/machines/backoff.d.ts +36 -0
  124. package/dist/machines/backoff.d.ts.map +1 -0
  125. package/dist/machines/backoff.js +34 -0
  126. package/dist/machines/backoff.js.map +1 -0
  127. package/dist/machines/bootstrap.d.ts +65 -0
  128. package/dist/machines/bootstrap.d.ts.map +1 -0
  129. package/dist/machines/bootstrap.js +105 -0
  130. package/dist/machines/bootstrap.js.map +1 -0
  131. package/dist/machines/chunks.d.ts +51 -0
  132. package/dist/machines/chunks.d.ts.map +1 -0
  133. package/dist/machines/chunks.js +67 -0
  134. package/dist/machines/chunks.js.map +1 -0
  135. package/dist/machines/index.d.ts +14 -0
  136. package/dist/machines/index.d.ts.map +1 -0
  137. package/dist/machines/index.js +14 -0
  138. package/dist/machines/index.js.map +1 -0
  139. package/dist/machines/journal.d.ts +63 -0
  140. package/dist/machines/journal.d.ts.map +1 -0
  141. package/dist/machines/journal.js +71 -0
  142. package/dist/machines/journal.js.map +1 -0
  143. package/dist/machines/ordering.d.ts +53 -0
  144. package/dist/machines/ordering.d.ts.map +1 -0
  145. package/dist/machines/ordering.js +87 -0
  146. package/dist/machines/ordering.js.map +1 -0
  147. package/dist/machines/presence.d.ts +26 -0
  148. package/dist/machines/presence.d.ts.map +1 -0
  149. package/dist/machines/presence.js +49 -0
  150. package/dist/machines/presence.js.map +1 -0
  151. package/dist/merge/index.d.ts +9 -0
  152. package/dist/merge/index.d.ts.map +1 -0
  153. package/dist/merge/index.js +9 -0
  154. package/dist/merge/index.js.map +1 -0
  155. package/dist/merge/merge3.d.ts +39 -0
  156. package/dist/merge/merge3.d.ts.map +1 -0
  157. package/dist/merge/merge3.js +188 -0
  158. package/dist/merge/merge3.js.map +1 -0
  159. package/package.json +48 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/detection/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,WAAW,CAAC;AAEnD,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAAG,QAAQ,CAEnE"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Amalgm's file-type policy: which physical type a piece of file ground
3
+ * takes. Ratified by entity-model.md ("Formats live underneath the file
4
+ * types"; text versus binary is an adapter difference, never an
5
+ * identity difference).
6
+ *
7
+ * Classification is an INJECTED policy everywhere it is used — the
8
+ * registration boundary and its lenses refuse to construct without one
9
+ * — so it lives in its own file with no other job: one definition,
10
+ * replaceable whole.
11
+ *
12
+ * The rule is the oldest one there is: bytes holding a NUL are not
13
+ * text. What matters is WHERE that fact comes from. It is not read
14
+ * here. Detect's single read of a file produces its fingerprint and
15
+ * this report together, and this policy only names the type that report
16
+ * implies.
17
+ *
18
+ * That is the whole law: A FILE'S HASH AND ITS TYPE DESCRIBE THE SAME
19
+ * OBSERVED BYTES. A policy that opened the ground itself would be a
20
+ * second look — and a second look can meet different bytes, or none at
21
+ * all, leaving a record whose type contradicts the payload beside it.
22
+ * So this function takes evidence, never a path; it is total, and it
23
+ * cannot fail.
24
+ */
25
+ export function classifyFile({ binary }) {
26
+ return binary ? 'file.binary' : 'file.text';
27
+ }
28
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/detection/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AASH,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAoB;IACvD,OAAO,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Root continuity verdicts: what a root's WITNESSED physical identity
3
+ * is, whether its recorded address still holds its ground, and whether
4
+ * a piece of ground rescues a moved root. Ratified by entity-model.md
5
+ * ("Ground that moves is rescued by continuity" — the directory law
6
+ * lifted to roots; "the follow rides witnessed continuity, never
7
+ * clairvoyance").
8
+ *
9
+ * Pure verdicts over injected answers — the host supplies the one look
10
+ * at an address (`statAt`) and the store's witnessed channel row
11
+ * (`channelIdentity`); the ACTIONS a verdict triggers (rebind, presence
12
+ * transitions, the undecided-address decision) stay in the host's
13
+ * wiring, because they re-aim watchers and scans.
14
+ */
15
+ import type { PhysicalIdentity } from './diff.js';
16
+ import type { RootKind } from './coverage.js';
17
+ /** What one look at an address found standing there. */
18
+ export type GroundKind = 'file' | 'dir' | 'link' | 'other';
19
+ export interface GroundStat extends PhysicalIdentity {
20
+ readonly kind: GroundKind;
21
+ }
22
+ export interface ContinuityRoot {
23
+ readonly rootId: string;
24
+ readonly path: string;
25
+ readonly kind: RootKind;
26
+ /** Directory roots: the root's own witnessed identity; null/absent when unwitnessed. */
27
+ readonly device?: number | null;
28
+ readonly inode?: number | null;
29
+ }
30
+ export interface ContinuityPorts {
31
+ /**
32
+ * One look at an address: the ground's physical identity and kind, or
33
+ * null when the look did not return ground. ANY failed look answers
34
+ * null here — at the evidence horizon's edge, "could not look" and
35
+ * "not there" both leave the address undecided, so this seam never
36
+ * needs the absent/unsettled split.
37
+ */
38
+ readonly statAt: (path: string) => GroundStat | null;
39
+ /**
40
+ * A single-ground root's one channel row identity — current as of the
41
+ * last completed scan — or null when unwitnessed. The file laws
42
+ * absorb atomic-save and retarget churn each time a scan witnesses
43
+ * the ground, and continuity can only ever claim what a scan has
44
+ * witnessed.
45
+ */
46
+ readonly channelIdentity: (rootId: string) => PhysicalIdentity | null;
47
+ }
48
+ export interface ContinuityVerdicts {
49
+ readonly rootIdentity: (root: ContinuityRoot) => PhysicalIdentity | null;
50
+ readonly findMovedRoot: (stat: GroundStat, roots: readonly ContinuityRoot[]) => ContinuityRoot | null;
51
+ readonly addressUndecided: (root: ContinuityRoot) => boolean;
52
+ }
53
+ export declare function createContinuity({ statAt, channelIdentity }: ContinuityPorts): ContinuityVerdicts;
54
+ //# sourceMappingURL=continuity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuity.d.ts","sourceRoot":"","sources":["../../src/detection/continuity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,UAAW,SAAQ,gBAAgB;IAClD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,gBAAgB,GAAG,IAAI,CAAC;CACvE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,gBAAgB,GAAG,IAAI,CAAC;IACzE,QAAQ,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,cAAc,EAAE,KAAK,cAAc,GAAG,IAAI,CAAC;IACtG,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC;CAC9D;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,kBAAkB,CAmEjG"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Root continuity verdicts: what a root's WITNESSED physical identity
3
+ * is, whether its recorded address still holds its ground, and whether
4
+ * a piece of ground rescues a moved root. Ratified by entity-model.md
5
+ * ("Ground that moves is rescued by continuity" — the directory law
6
+ * lifted to roots; "the follow rides witnessed continuity, never
7
+ * clairvoyance").
8
+ *
9
+ * Pure verdicts over injected answers — the host supplies the one look
10
+ * at an address (`statAt`) and the store's witnessed channel row
11
+ * (`channelIdentity`); the ACTIONS a verdict triggers (rebind, presence
12
+ * transitions, the undecided-address decision) stay in the host's
13
+ * wiring, because they re-aim watchers and scans.
14
+ */
15
+ export function createContinuity({ statAt, channelIdentity }) {
16
+ /**
17
+ * A root's WITNESSED physical identity, for continuity rescue.
18
+ * Directory roots carry their own device:inode (nothing replaces a
19
+ * directory's inode in place). A single-ground root's identity lives
20
+ * in its one channel row. Ground that changed inodes inside a
21
+ * still-open settle window is unwitnessed — no evidence, no claim.
22
+ */
23
+ function rootIdentity(root) {
24
+ if (root.kind !== 'file') {
25
+ const { device, inode } = root;
26
+ return device == null || inode == null ? null : { device, inode };
27
+ }
28
+ return channelIdentity(root.rootId);
29
+ }
30
+ /** Orphaned means the recorded address no longer holds this ground. */
31
+ function orphanedAtAddress(root) {
32
+ const identity = rootIdentity(root);
33
+ if (!identity)
34
+ return false;
35
+ const stat = statAt(root.path);
36
+ if (!stat)
37
+ return true;
38
+ return stat.device !== identity.device || stat.inode !== identity.inode;
39
+ }
40
+ /**
41
+ * Continuity claims a root exactly like it claims the directories
42
+ * inside one: unique device:inode match, exactly one candidate, and
43
+ * only for a path-orphan — a root whose recorded address still holds
44
+ * its ground is never stolen by a lookalike.
45
+ */
46
+ function findMovedRoot(stat, roots) {
47
+ const wantDir = stat.kind === 'dir';
48
+ const matches = roots.filter((root) => {
49
+ if (wantDir !== (root.kind !== 'file'))
50
+ return false;
51
+ const identity = rootIdentity(root);
52
+ return identity !== null && identity.device === stat.device && identity.inode === stat.inode;
53
+ });
54
+ const only = matches.length === 1 ? matches[0] : null;
55
+ return only && orphanedAtAddress(only) ? only : null;
56
+ }
57
+ /**
58
+ * A scan's evidence horizon is the root's ground; the root's own
59
+ * address is the horizon's edge. Ground missing there — or a
60
+ * directory root's address wearing a different physical identity — is
61
+ * UNDECIDABLE from here: moved away, deleted, or replaced by a
62
+ * lookalike all present the same address, and concluding "deleted"
63
+ * would end every channel a rescue could have saved. So the scan
64
+ * concludes nothing: beliefs freeze, the referee convicts the
65
+ * divergence, and the next enrollment moment decides — naming the new
66
+ * address rescues, re-naming this one adopts, and explicit removal is
67
+ * the only deletion (an external filesystem change is never
68
+ * permission to end identity). A single-ground root's address
69
+ * lawfully churns inodes (atomic saves, retargets), so only absence —
70
+ * or foreign directory ground — freezes it; presence is the file
71
+ * laws' business.
72
+ */
73
+ function addressUndecided(root) {
74
+ const stat = statAt(root.path);
75
+ if (!stat)
76
+ return true;
77
+ if (root.kind === 'file')
78
+ return stat.kind !== 'file' && stat.kind !== 'link';
79
+ if (stat.kind !== 'dir')
80
+ return true;
81
+ return root.device != null && (stat.device !== root.device || stat.inode !== root.inode);
82
+ }
83
+ return { rootIdentity, findMovedRoot, addressUndecided };
84
+ }
85
+ //# sourceMappingURL=continuity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuity.js","sourceRoot":"","sources":["../../src/detection/continuity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8CH,MAAM,UAAU,gBAAgB,CAAC,EAAE,MAAM,EAAE,eAAe,EAAmB;IAC3E;;;;;;OAMG;IACH,SAAS,YAAY,CAAC,IAAoB;QACxC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;YAC/B,OAAO,MAAM,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACpE,CAAC;QACD,OAAO,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,uEAAuE;IACvE,SAAS,iBAAiB,CAAC,IAAoB;QAC7C,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,KAAK,CAAC;IAC1E,CAAC;IAED;;;;;OAKG;IACH,SAAS,aAAa,CAAC,IAAgB,EAAE,KAAgC;QACvE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YACpC,IAAI,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;gBAAE,OAAO,KAAK,CAAC;YACrD,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;YACpC,OAAO,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC;QAC/F,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,OAAO,IAAI,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACvD,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,SAAS,gBAAgB,CAAC,IAAoB;QAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;QAC9E,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3F,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The coverage law: one OS watcher per outermost tree. Ratified by
3
+ * entity-model.md (enclosed-ground coverage) and
4
+ * entity-implementation.md (the doorbell matrix).
5
+ *
6
+ * A recursive watcher on a directory hears every event beneath it, at
7
+ * any depth — so a root enclosed by watched ground already has its
8
+ * content observed, exactly as its ADDRESS is already observed. Opening
9
+ * a second watcher over enclosed ground buys nothing on any platform
10
+ * and, on Linux — where a recursive watch is silently one inotify slot
11
+ * PER DIRECTORY against a small kernel budget — exhausts the kernel.
12
+ * Doorbells are shared; what a root DOES with its dings (files
13
+ * channels, repo Card+Checkpoint) stays its own type's business.
14
+ *
15
+ * Pure functions over root rows — no handles, no state. The host owns
16
+ * opening and closing; this module only answers "who guards whom".
17
+ * Addresses are absolute '/'-separated paths with no trailing
18
+ * separator, as the enrollment boundary records them.
19
+ */
20
+ /** The three root kinds; only 'file' changes a doorbell's ground. */
21
+ export type RootKind = 'file' | 'files' | 'repo';
22
+ export interface WatchRoot {
23
+ readonly rootId: string;
24
+ readonly path: string;
25
+ readonly kind: RootKind;
26
+ }
27
+ /**
28
+ * The ground a root's doorbell must cover: a directory root guards
29
+ * itself; a single-file root's bell lives on its parent directory (the
30
+ * file's own inode goes deaf on atomic saves).
31
+ */
32
+ export declare function doorbellGround(root: WatchRoot): string;
33
+ /**
34
+ * The root whose OS watcher covers `root`'s doorbell ground, or null
35
+ * when `root` guards itself. The OUTERMOST enclosing directory root
36
+ * wins: with A ⊃ B ⊃ C only A holds a handle, and every deeper root —
37
+ * B, C, nested repos, single files — rides it. A file root can never
38
+ * cover anything.
39
+ */
40
+ export declare function watchOwnerOf(root: WatchRoot, roots: readonly WatchRoot[]): WatchRoot | null;
41
+ /** Every live root covered by `owner` (owner excluded). */
42
+ export declare function coveredRoots(owner: WatchRoot, roots: readonly WatchRoot[]): WatchRoot[];
43
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/detection/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,qEAAqE;AACrE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AASD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,GAAG,IAAI,CAS3F;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,EAAE,CAGvF"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The coverage law: one OS watcher per outermost tree. Ratified by
3
+ * entity-model.md (enclosed-ground coverage) and
4
+ * entity-implementation.md (the doorbell matrix).
5
+ *
6
+ * A recursive watcher on a directory hears every event beneath it, at
7
+ * any depth — so a root enclosed by watched ground already has its
8
+ * content observed, exactly as its ADDRESS is already observed. Opening
9
+ * a second watcher over enclosed ground buys nothing on any platform
10
+ * and, on Linux — where a recursive watch is silently one inotify slot
11
+ * PER DIRECTORY against a small kernel budget — exhausts the kernel.
12
+ * Doorbells are shared; what a root DOES with its dings (files
13
+ * channels, repo Card+Checkpoint) stays its own type's business.
14
+ *
15
+ * Pure functions over root rows — no handles, no state. The host owns
16
+ * opening and closing; this module only answers "who guards whom".
17
+ * Addresses are absolute '/'-separated paths with no trailing
18
+ * separator, as the enrollment boundary records them.
19
+ */
20
+ /** '/a/b' → '/a'; '/a' → '/'; a bare name → '.'. */
21
+ const parentDirOf = (path) => {
22
+ const cut = path.lastIndexOf('/');
23
+ if (cut < 0)
24
+ return '.';
25
+ return cut === 0 ? '/' : path.slice(0, cut);
26
+ };
27
+ /**
28
+ * The ground a root's doorbell must cover: a directory root guards
29
+ * itself; a single-file root's bell lives on its parent directory (the
30
+ * file's own inode goes deaf on atomic saves).
31
+ */
32
+ export function doorbellGround(root) {
33
+ return root.kind === 'file' ? parentDirOf(root.path) : root.path;
34
+ }
35
+ /**
36
+ * The root whose OS watcher covers `root`'s doorbell ground, or null
37
+ * when `root` guards itself. The OUTERMOST enclosing directory root
38
+ * wins: with A ⊃ B ⊃ C only A holds a handle, and every deeper root —
39
+ * B, C, nested repos, single files — rides it. A file root can never
40
+ * cover anything.
41
+ */
42
+ export function watchOwnerOf(root, roots) {
43
+ const ground = doorbellGround(root);
44
+ let owner = null;
45
+ for (const other of roots) {
46
+ if (other.rootId === root.rootId || other.kind === 'file')
47
+ continue;
48
+ if (ground !== other.path && !ground.startsWith(`${other.path}/`))
49
+ continue;
50
+ if (!owner || other.path.length < owner.path.length)
51
+ owner = other;
52
+ }
53
+ return owner;
54
+ }
55
+ /** Every live root covered by `owner` (owner excluded). */
56
+ export function coveredRoots(owner, roots) {
57
+ return roots.filter((root) => root.rootId !== owner.rootId
58
+ && watchOwnerOf(root, roots)?.rootId === owner.rootId);
59
+ }
60
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/detection/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAWH,oDAAoD;AACpD,MAAM,WAAW,GAAG,CAAC,IAAY,EAAU,EAAE;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACxB,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAe;IAC5C,OAAO,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAe,EAAE,KAA2B;IACvE,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,KAAK,GAAqB,IAAI,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,SAAS;QACpE,IAAI,MAAM,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;YAAE,SAAS;QAC5E,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM;YAAE,KAAK,GAAG,KAAK,CAAC;IACrE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,YAAY,CAAC,KAAgB,EAAE,KAA2B;IACxE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;WACrD,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The compare core of detection: rows (store memory) + entries (disk
3
+ * truth) in, actions out. Pure — the walk that produces entries and the
4
+ * hashing that settles `checks` are the host's; only the identity
5
+ * verdicts live here. Ratified by entity-model.md ("Address and
6
+ * physical continuity are the two identity witnesses, never content").
7
+ *
8
+ * Three kinds of channel, three orderings of the same two witnesses:
9
+ *
10
+ * - Files: path claims first; the inode rescues path-orphans. An
11
+ * atomic save replaces a file's inode in place, so the address must
12
+ * absorb that churn.
13
+ * - Directories: physical continuity claims first; the address claims
14
+ * what no inode claims. Nothing replaces a directory's inode in
15
+ * place, so a known inode at a new address IS the directory, moved.
16
+ * - Links: the FILE rules, for the file reason — retarget is unlink +
17
+ * recreate, so the inode churns exactly like an atomic save.
18
+ */
19
+ /** One piece of ground's physical identity: device:inode. */
20
+ export interface PhysicalIdentity {
21
+ readonly device: number;
22
+ readonly inode: number;
23
+ }
24
+ /** A row and an entry the compare has bound to one channel. */
25
+ export interface Match<Row, Entry> {
26
+ readonly row: Row;
27
+ readonly entry: Entry;
28
+ }
29
+ /** Disk truth for one file, as the walk reported it. */
30
+ export interface FileEntry extends PhysicalIdentity {
31
+ readonly relPath: string;
32
+ readonly size: number;
33
+ readonly mtimeMs: number;
34
+ }
35
+ /** Store memory for one file channel. `hashedAtMs` is the instant the bytes were last read. */
36
+ export interface FileRow extends PhysicalIdentity {
37
+ readonly fileId: string;
38
+ readonly relPath: string;
39
+ readonly size: number;
40
+ readonly mtimeMs: number;
41
+ readonly hashedAtMs?: number | null;
42
+ }
43
+ export interface DirEntry extends PhysicalIdentity {
44
+ readonly relPath: string;
45
+ }
46
+ export interface DirRow extends PhysicalIdentity {
47
+ readonly dirId: string;
48
+ readonly relPath: string;
49
+ }
50
+ /** Disk truth for one symlink: the written target text, read at walk time, never followed. */
51
+ export interface LinkEntry extends PhysicalIdentity {
52
+ readonly relPath: string;
53
+ readonly target: string;
54
+ }
55
+ export interface LinkRow extends PhysicalIdentity {
56
+ readonly linkId: string;
57
+ readonly relPath: string;
58
+ readonly target: string;
59
+ }
60
+ /**
61
+ * Physical continuity, shared by every compare function: a row and an
62
+ * entry sharing one device:inode, exactly one match on both sides → the
63
+ * same physical thing. Anything else is never guessed. For files this
64
+ * rescues path-orphans (rule 2); for directories it IS rule 1.
65
+ */
66
+ export declare function matchByInode<Row extends PhysicalIdentity, Entry extends PhysicalIdentity>(rows: readonly Row[], entries: readonly Entry[]): Array<Match<Row, Entry>>;
67
+ export interface DirDiff {
68
+ readonly renames: Array<Match<DirRow, DirEntry>>;
69
+ readonly retags: Array<Match<DirRow, DirEntry>>;
70
+ readonly creates: DirEntry[];
71
+ readonly deletes: DirRow[];
72
+ }
73
+ /**
74
+ * The compare function for directory channels — the file identity rules
75
+ * in the OPPOSITE order, for the mirror reason. A file's inode is
76
+ * replaced in place on every atomic save, so for files the path must
77
+ * absorb that churn (path claims first). Nothing replaces a directory's
78
+ * inode in place: a known inode at a new address IS the directory,
79
+ * moved — even when its old address was reoccupied in the same interval
80
+ * (two directories exchanging names cross by inode, exactly like the
81
+ * files inside them). So:
82
+ * 1. Physical continuity claims the channel: unique device:inode,
83
+ * whole sets, same path = continued, new path = rename.
84
+ * 2. The address claims what no inode claims — recreation under an old
85
+ * name is the same channel wearing a never-seen inode (`retags`,
86
+ * silent).
87
+ * 3. Leftover entries are creates; leftover rows are deletes.
88
+ * No content: a directory's membership is derived state upstream, so
89
+ * there is nothing to hash or check.
90
+ */
91
+ export declare function diffDirState(rows: readonly DirRow[], entries: readonly DirEntry[]): DirDiff;
92
+ export interface LinkDiff {
93
+ readonly changes: Array<Match<LinkRow, LinkEntry>>;
94
+ readonly retags: Array<Match<LinkRow, LinkEntry>>;
95
+ readonly renames: Array<Match<LinkRow, LinkEntry>>;
96
+ readonly creates: LinkEntry[];
97
+ readonly deletes: LinkRow[];
98
+ }
99
+ /**
100
+ * The compare function for link channels — the FILE identity rules, for
101
+ * the file reason: a symlink cannot be retargeted in place (retarget is
102
+ * unlink + recreate, so the inode churns exactly like an atomic save),
103
+ * so the path absorbs that churn and claims first; the inode rescues
104
+ * path-orphans (`mv` preserves a symlink's inode). The payload is the
105
+ * target string, already read at walk time. A consequence worth naming:
106
+ * two links exchanging names read as two retargets (path-first),
107
+ * because a link's meaning IS its written address.
108
+ * 1. Path claims the channel: same target = continued (a new inode
109
+ * under an old name with an old target is silent recreation),
110
+ * different target = changed.
111
+ * 2. Inode rescues path-orphans: renames.
112
+ * 3. Leftover entries are creates; leftover rows are deletes.
113
+ */
114
+ export declare function diffLinkState(rows: readonly LinkRow[], entries: readonly LinkEntry[]): LinkDiff;
115
+ export interface RootDiff {
116
+ /** Files whose content must be read before an event may be claimed. */
117
+ readonly checks: Array<Match<FileRow, FileEntry>>;
118
+ readonly renames: Array<Match<FileRow, FileEntry>>;
119
+ readonly creates: FileEntry[];
120
+ readonly deletes: FileRow[];
121
+ readonly unchanged: number;
122
+ }
123
+ /**
124
+ * The compare function for file channels — doorbells and rescans both
125
+ * end here. Hashing is deferred — `checks` name the files whose content
126
+ * must be read before an event may be claimed.
127
+ *
128
+ * Identity rules, in order:
129
+ * 1. Path claims the channel. Same rel_path = same file_id, even when
130
+ * the inode changed (atomic saves replace the inode on every
131
+ * write).
132
+ * 2. Inode rescues path-orphans (matchByInode above).
133
+ * 3. Leftover entries are creates; leftover rows are deletes.
134
+ *
135
+ * The single stat-shortcut this layer allows itself: a (size, mtime,
136
+ * inode) triple identical to the stored one is skipped as unchanged —
137
+ * but only when the mtime is strictly older than the moment we last
138
+ * read the bytes (git's "racily clean" rule), and only when nothing
139
+ * pointed at the file. A path named in `mustCheck` (the doorbell said
140
+ * so) is always hashed: timestamps can lie, a delivered watcher event
141
+ * cannot be un-delivered.
142
+ */
143
+ export declare function diffRootState(rows: readonly FileRow[], entries: readonly FileEntry[], mustCheck?: ReadonlySet<string> | null): RootDiff;
144
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/detection/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The compare core of detection: rows (store memory) + entries (disk
3
+ * truth) in, actions out. Pure — the walk that produces entries and the
4
+ * hashing that settles `checks` are the host's; only the identity
5
+ * verdicts live here. Ratified by entity-model.md ("Address and
6
+ * physical continuity are the two identity witnesses, never content").
7
+ *
8
+ * Three kinds of channel, three orderings of the same two witnesses:
9
+ *
10
+ * - Files: path claims first; the inode rescues path-orphans. An
11
+ * atomic save replaces a file's inode in place, so the address must
12
+ * absorb that churn.
13
+ * - Directories: physical continuity claims first; the address claims
14
+ * what no inode claims. Nothing replaces a directory's inode in
15
+ * place, so a known inode at a new address IS the directory, moved.
16
+ * - Links: the FILE rules, for the file reason — retarget is unlink +
17
+ * recreate, so the inode churns exactly like an atomic save.
18
+ */
19
+ /**
20
+ * Physical continuity, shared by every compare function: a row and an
21
+ * entry sharing one device:inode, exactly one match on both sides → the
22
+ * same physical thing. Anything else is never guessed. For files this
23
+ * rescues path-orphans (rule 2); for directories it IS rule 1.
24
+ */
25
+ export function matchByInode(rows, entries) {
26
+ const one = (map, key, value) => map.set(key, map.has(key) ? 'many' : value);
27
+ const rowsByInode = new Map();
28
+ for (const row of rows)
29
+ one(rowsByInode, `${row.device}:${row.inode}`, row);
30
+ const entriesByInode = new Map();
31
+ for (const entry of entries)
32
+ one(entriesByInode, `${entry.device}:${entry.inode}`, entry);
33
+ const matched = [];
34
+ for (const [key, entry] of entriesByInode) {
35
+ const row = rowsByInode.get(key);
36
+ if (entry === 'many' || !row || row === 'many')
37
+ continue;
38
+ matched.push({ row, entry });
39
+ }
40
+ return matched;
41
+ }
42
+ /**
43
+ * The compare function for directory channels — the file identity rules
44
+ * in the OPPOSITE order, for the mirror reason. A file's inode is
45
+ * replaced in place on every atomic save, so for files the path must
46
+ * absorb that churn (path claims first). Nothing replaces a directory's
47
+ * inode in place: a known inode at a new address IS the directory,
48
+ * moved — even when its old address was reoccupied in the same interval
49
+ * (two directories exchanging names cross by inode, exactly like the
50
+ * files inside them). So:
51
+ * 1. Physical continuity claims the channel: unique device:inode,
52
+ * whole sets, same path = continued, new path = rename.
53
+ * 2. The address claims what no inode claims — recreation under an old
54
+ * name is the same channel wearing a never-seen inode (`retags`,
55
+ * silent).
56
+ * 3. Leftover entries are creates; leftover rows are deletes.
57
+ * No content: a directory's membership is derived state upstream, so
58
+ * there is nothing to hash or check.
59
+ */
60
+ export function diffDirState(rows, entries) {
61
+ const matched = matchByInode(rows, entries);
62
+ const matchedRowIds = new Set(matched.map((pair) => pair.row.dirId));
63
+ const matchedEntries = new Set(matched.map((pair) => pair.entry));
64
+ const rowsByPath = new Map(rows.filter((row) => !matchedRowIds.has(row.dirId))
65
+ .map((row) => [row.relPath, row]));
66
+ const retags = [];
67
+ const creates = [];
68
+ for (const entry of entries) {
69
+ if (matchedEntries.has(entry))
70
+ continue;
71
+ const row = rowsByPath.get(entry.relPath);
72
+ if (row) {
73
+ rowsByPath.delete(entry.relPath);
74
+ retags.push({ row, entry });
75
+ }
76
+ else {
77
+ creates.push(entry);
78
+ }
79
+ }
80
+ return {
81
+ renames: matched.filter((pair) => pair.row.relPath !== pair.entry.relPath),
82
+ retags,
83
+ creates,
84
+ deletes: [...rowsByPath.values()],
85
+ };
86
+ }
87
+ /**
88
+ * The compare function for link channels — the FILE identity rules, for
89
+ * the file reason: a symlink cannot be retargeted in place (retarget is
90
+ * unlink + recreate, so the inode churns exactly like an atomic save),
91
+ * so the path absorbs that churn and claims first; the inode rescues
92
+ * path-orphans (`mv` preserves a symlink's inode). The payload is the
93
+ * target string, already read at walk time. A consequence worth naming:
94
+ * two links exchanging names read as two retargets (path-first),
95
+ * because a link's meaning IS its written address.
96
+ * 1. Path claims the channel: same target = continued (a new inode
97
+ * under an old name with an old target is silent recreation),
98
+ * different target = changed.
99
+ * 2. Inode rescues path-orphans: renames.
100
+ * 3. Leftover entries are creates; leftover rows are deletes.
101
+ */
102
+ export function diffLinkState(rows, entries) {
103
+ const changes = [];
104
+ const retags = [];
105
+ const rowsByPath = new Map(rows.map((row) => [row.relPath, row]));
106
+ const claimedRowIds = new Set();
107
+ const orphanEntries = [];
108
+ for (const entry of entries) {
109
+ const row = rowsByPath.get(entry.relPath);
110
+ if (!row) {
111
+ orphanEntries.push(entry);
112
+ continue;
113
+ }
114
+ claimedRowIds.add(row.linkId);
115
+ if (row.target !== entry.target)
116
+ changes.push({ row, entry });
117
+ else if (row.device !== entry.device || row.inode !== entry.inode)
118
+ retags.push({ row, entry });
119
+ }
120
+ const orphanRows = rows.filter((row) => !claimedRowIds.has(row.linkId));
121
+ const renames = matchByInode(orphanRows, orphanEntries);
122
+ const renamedRowIds = new Set(renames.map((pair) => pair.row.linkId));
123
+ const renamedEntries = new Set(renames.map((pair) => pair.entry));
124
+ return {
125
+ changes,
126
+ retags,
127
+ renames,
128
+ creates: orphanEntries.filter((entry) => !renamedEntries.has(entry)),
129
+ deletes: orphanRows.filter((row) => !renamedRowIds.has(row.linkId)),
130
+ };
131
+ }
132
+ /**
133
+ * The compare function for file channels — doorbells and rescans both
134
+ * end here. Hashing is deferred — `checks` name the files whose content
135
+ * must be read before an event may be claimed.
136
+ *
137
+ * Identity rules, in order:
138
+ * 1. Path claims the channel. Same rel_path = same file_id, even when
139
+ * the inode changed (atomic saves replace the inode on every
140
+ * write).
141
+ * 2. Inode rescues path-orphans (matchByInode above).
142
+ * 3. Leftover entries are creates; leftover rows are deletes.
143
+ *
144
+ * The single stat-shortcut this layer allows itself: a (size, mtime,
145
+ * inode) triple identical to the stored one is skipped as unchanged —
146
+ * but only when the mtime is strictly older than the moment we last
147
+ * read the bytes (git's "racily clean" rule), and only when nothing
148
+ * pointed at the file. A path named in `mustCheck` (the doorbell said
149
+ * so) is always hashed: timestamps can lie, a delivered watcher event
150
+ * cannot be un-delivered.
151
+ */
152
+ export function diffRootState(rows, entries, mustCheck = null) {
153
+ const checks = [];
154
+ let unchanged = 0;
155
+ const rowsByPath = new Map(rows.map((row) => [row.relPath, row]));
156
+ const claimedRowIds = new Set();
157
+ const orphanEntries = [];
158
+ for (const entry of entries) {
159
+ const row = rowsByPath.get(entry.relPath);
160
+ if (!row) {
161
+ orphanEntries.push(entry);
162
+ continue;
163
+ }
164
+ claimedRowIds.add(row.fileId);
165
+ const statsMatch = row.size === entry.size && row.mtimeMs === entry.mtimeMs && row.inode === entry.inode;
166
+ const provablyBeforeLastRead = typeof row.hashedAtMs === 'number' && entry.mtimeMs < row.hashedAtMs;
167
+ const named = mustCheck !== null && mustCheck.has(entry.relPath);
168
+ if (statsMatch && provablyBeforeLastRead && !named) {
169
+ unchanged += 1;
170
+ }
171
+ else {
172
+ checks.push({ row, entry });
173
+ }
174
+ }
175
+ const orphanRows = rows.filter((row) => !claimedRowIds.has(row.fileId));
176
+ const renames = matchByInode(orphanRows, orphanEntries);
177
+ const renamedRowIds = new Set(renames.map((pair) => pair.row.fileId));
178
+ const renamedEntries = new Set(renames.map((pair) => pair.entry));
179
+ return {
180
+ checks,
181
+ renames,
182
+ creates: orphanEntries.filter((entry) => !renamedEntries.has(entry)),
183
+ deletes: orphanRows.filter((row) => !renamedRowIds.has(row.fileId)),
184
+ unchanged,
185
+ };
186
+ }
187
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/detection/diff.ts"],"names":[],"mappings":"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"}