@codyswann/lisa 3.14.0 → 3.14.2

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 (91) hide show
  1. package/all/copy-overwrite/scripts/lisa-destructive-guard.mjs +3 -0
  2. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +3 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +3 -0
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-instruction-file-edits.sh +3 -0
  5. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +3 -0
  6. package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +3 -0
  7. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +3 -0
  8. package/all/copy-overwrite/scripts/lisa-hooks/sonar-secrets.sh +3 -0
  9. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +151 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/lisa-owned-templates.d.ts +40 -6
  13. package/dist/core/lisa-owned-templates.d.ts.map +1 -1
  14. package/dist/core/lisa-owned-templates.js +47 -10
  15. package/dist/core/lisa-owned-templates.js.map +1 -1
  16. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  17. package/dist/core/upstream-evidence-manifest.js +33 -29
  18. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  19. package/expo/copy-overwrite/.easignore.extra +3 -0
  20. package/package.json +1 -1
  21. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-agy/plugin.json +1 -1
  34. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  54. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-agy/plugin.json +1 -1
  59. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  64. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  69. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  71. package/rails/copy-overwrite/.rubocop.yml +3 -0
  72. package/rails/copy-overwrite/ast-grep/rules/ruby/no-find-by-sql-without-params.yml +3 -0
  73. package/rails/copy-overwrite/ast-grep/rules/ruby/no-params-without-permit.yml +3 -0
  74. package/rails/copy-overwrite/ast-grep/rules/ruby/no-raw-sql-in-where.yml +3 -0
  75. package/rails/copy-overwrite/ast-grep/rules/ruby/no-skip-before-action-without-scope.yml +3 -0
  76. package/rails/copy-overwrite/ast-grep/rules/ruby/no-unsafe-send.yml +3 -0
  77. package/rails/copy-overwrite/ast-grep/rules/ruby/no-update-columns.yml +3 -0
  78. package/rails/copy-overwrite/config/initializers/version.rb +2 -0
  79. package/rails/copy-overwrite/lefthook.yml +3 -0
  80. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +3 -0
  81. package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -0
  82. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +3 -0
  83. package/scripts/build-plugins.sh +23 -7
  84. package/scripts/generate-lisa-owned-hash-ledger.mjs +18 -12
  85. package/scripts/materialize-copy-overwrite.mjs +163 -0
  86. package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +3 -0
  87. package/typescript/copy-overwrite/ast-grep/rules/no-inline-component-in-container.yml +3 -0
  88. package/typescript/copy-overwrite/ast-grep/rules/no-inline-component-in-view.yml +3 -0
  89. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +3 -0
  90. package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -0
  91. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +3 -0
@@ -12,6 +12,20 @@ SRC_DIR="$PLUGINS_DIR/src"
12
12
  # Read version from package.json so plugins stay in sync with Lisa releases
13
13
  VERSION=$(node -e "console.log(require('$ROOT_DIR/package.json').version)")
14
14
 
15
+ # Materialize a Lisa-owned source file into a copy-overwrite/ template tree.
16
+ #
17
+ # Never a plain `cp`. These destinations are `copy-overwrite` assets, and a
18
+ # `copy-overwrite` asset that reads as editable silently loses downstream
19
+ # hardening on the next sync — already observed on block-no-verify.sh. They are
20
+ # also the only assets that cannot carry a hand-typed ownership header, because
21
+ # this script would erase it on the next build (#2547). So the header is stamped
22
+ # here, as the file is generated: the authored source stays honest about being
23
+ # editable, the shipped copy states that it is replaced, and the two cannot
24
+ # disagree because one produces the other.
25
+ materialize() {
26
+ node "$ROOT_DIR/scripts/materialize-copy-overwrite.mjs" "$1" "$2"
27
+ }
28
+
15
29
  inject_version() {
16
30
  local manifest="$1"
17
31
  if [ -f "$manifest" ]; then
@@ -82,11 +96,12 @@ if [ -f "$SRC_DIR/base/hooks/threshold-ratchet.mjs" ]; then
82
96
  mkdir -p "$ratchet_scripts_dir"
83
97
  # The entry point takes the template check-* naming; its relative imports
84
98
  # (threshold-ratchet-*.mjs) keep their canonical names in both trees.
85
- cp "$SRC_DIR/base/hooks/threshold-ratchet.mjs" \
99
+ materialize "$SRC_DIR/base/hooks/threshold-ratchet.mjs" \
86
100
  "$ratchet_scripts_dir/check-threshold-ratchet.mjs"
87
- cp "$SRC_DIR/base/hooks/threshold-ratchet-families.mjs" \
88
- "$SRC_DIR/base/hooks/threshold-ratchet-compare.mjs" \
89
- "$ratchet_scripts_dir/"
101
+ for ratchet_module in threshold-ratchet-families threshold-ratchet-compare; do
102
+ materialize "$SRC_DIR/base/hooks/$ratchet_module.mjs" \
103
+ "$ratchet_scripts_dir/$ratchet_module.mjs"
104
+ done
90
105
  done
91
106
  fi
92
107
 
@@ -108,7 +123,7 @@ fi
108
123
  for guard in block-no-verify parity-safety-net block-shell-json-parsing \
109
124
  block-instruction-file-edits block-direct-issue-create; do
110
125
  if [ -f "$SRC_DIR/base/hooks/$guard.sh" ]; then
111
- cp "$SRC_DIR/base/hooks/$guard.sh" "$HOST_GUARD_DIR/$guard.sh"
126
+ materialize "$SRC_DIR/base/hooks/$guard.sh" "$HOST_GUARD_DIR/$guard.sh"
112
127
  chmod +x "$HOST_GUARD_DIR/$guard.sh"
113
128
  fi
114
129
  done
@@ -122,7 +137,8 @@ done
122
137
  # guards are, not because it is dispatched the same way.
123
138
  if [ -f "$SRC_DIR/base/hooks/sonar-secrets.sh" ]; then
124
139
  mkdir -p "$HOST_GUARD_DIR"
125
- cp "$SRC_DIR/base/hooks/sonar-secrets.sh" "$HOST_GUARD_DIR/sonar-secrets.sh"
140
+ materialize "$SRC_DIR/base/hooks/sonar-secrets.sh" \
141
+ "$HOST_GUARD_DIR/sonar-secrets.sh"
126
142
  chmod +x "$HOST_GUARD_DIR/sonar-secrets.sh"
127
143
  fi
128
144
 
@@ -132,7 +148,7 @@ fi
132
148
  # and an unconditional copy fails the whole build there.
133
149
  if [ -f "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" ]; then
134
150
  mkdir -p "$ROOT_DIR/all/copy-overwrite/scripts"
135
- cp "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" \
151
+ materialize "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" \
136
152
  "$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
137
153
  chmod +x "$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
138
154
  fi
@@ -28,6 +28,7 @@ const repoRoot = path.resolve(import.meta.dirname, "..");
28
28
  const outputPath = path.join(repoRoot, "src/core/lisa-owned-hash-ledger.ts");
29
29
  const COPY_OVERWRITE = "copy-overwrite";
30
30
  const LISA_NAMESPACE_PREFIX = "lisa-";
31
+ const ENFORCEMENT_TREE = "scripts/";
31
32
 
32
33
  /**
33
34
  * Run a git command in the repository and return stdout as latin1 text.
@@ -51,27 +52,32 @@ function git(args) {
51
52
  }
52
53
 
53
54
  /**
54
- * Whether a destination path sits in the namespace Lisa owns outright.
55
+ * Whether a destination path sits in the territory Lisa owns outright.
55
56
  *
56
57
  * Mirrors `isLisaOwnedTemplate` in `src/core/lisa-owned-templates.ts`. The
57
58
  * duplication is deliberate — this script runs before the TypeScript build, so
58
59
  * it cannot import the compiled module — and a test asserts the two agree.
59
60
  *
60
- * This is also the ledger's boundary, and it is inherited rather than chosen.
61
- * The `lisa-` path segment is what decides which files refresh at all, so a file
62
- * outside it is never refreshed after its initial create and has nothing for
63
- * provenance to deciderecording hashes for it would add a regeneration gate
64
- * to ~120 more templates and buy nothing today. If that predicate is ever
65
- * replaced (CodySwannGT/lisa#2491 tracks it freezing non-`lisa-` guards at
66
- * whatever version first landed), widening enrolment is this one function: the
67
- * ledger answers "has Lisa ever shipped these bytes here?" for any managed path.
61
+ * This is also the ledger's boundary, and it is inherited rather than chosen:
62
+ * whatever refresh acts on is what provenance has to be able to judge. That is
63
+ * why the two widened together in CodySwannGT/lisa#2551. This function's earlier
64
+ * self predicted it"widening enrolment is this one function" and the note
65
+ * is worth keeping, because the direction of the dependency is the load-bearing
66
+ * part: enrolment must never lag the predicate. `classifyHostCopy` returns
67
+ * `unenrolled` for a path it has no hashes for, and refresh reads that as
68
+ * permission to overwrite, so a file the predicate newly owns but the ledger has
69
+ * not recorded is not merely unprotected — it is clobbered on the next apply.
70
+ * A test asserts the containment in that direction so the two cannot drift.
68
71
  * @param {string} destination - Repo-relative destination path
69
72
  * @returns {boolean} True when Lisa owns the file outright
70
73
  */
71
74
  function isLisaOwned(destination) {
72
- return destination
73
- .split("/")
74
- .some(segment => segment.startsWith(LISA_NAMESPACE_PREFIX));
75
+ return (
76
+ destination.startsWith(ENFORCEMENT_TREE) ||
77
+ destination
78
+ .split("/")
79
+ .some(segment => segment.startsWith(LISA_NAMESPACE_PREFIX))
80
+ );
75
81
  }
76
82
 
77
83
  /**
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy a Lisa-owned file into a `copy-overwrite/` template tree, stamping the
4
+ * ownership header on the way in.
5
+ *
6
+ * Thirteen `copy-overwrite` assets are not authored where they ship. The build
7
+ * materializes them from `plugins/src/base/hooks/` (the five PreToolUse guards,
8
+ * the Sonar wrapper, the three threshold-ratchet modules into two stack lanes)
9
+ * and from `scripts/lisa-enforcement-fallback.sh` (the dispatcher). A header
10
+ * typed into the shipped copy is erased by the next `bun run build`, which is
11
+ * why those thirteen were the only files #2545 could not correct.
12
+ *
13
+ * The header cannot move upstream to the source either. `plugins/src/base/hooks/
14
+ * block-no-verify.sh` is precisely the file maintainers edit, so telling it that
15
+ * it will be overwritten would be a fresh instance of the false statement #2538
16
+ * exists to remove. So the header becomes a property of the *generation step*:
17
+ * the source stays honest, the shipped copy states its real contract, and the
18
+ * two can never disagree because one produces the other.
19
+ *
20
+ * This matters most on exactly these files. A `copy-overwrite` asset that reads
21
+ * as editable silently loses downstream hardening — a consumer repo hardened its
22
+ * `block-no-verify.sh` and the next sync reverted it, noticed only because two
23
+ * of its own tests started failing.
24
+ * @module scripts/materialize-copy-overwrite
25
+ */
26
+ import { readFileSync, writeFileSync } from "node:fs";
27
+ import path from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ /**
31
+ * The `copy-overwrite` ownership contract, verbatim from the header #2545
32
+ * established. Duplicated nowhere: `tests/unit/templates/
33
+ * template-ownership-header.test.ts` asserts the same two sentences over every
34
+ * template in the lane, and this generator writes them into the thirteen that
35
+ * cannot hold a hand-typed copy.
36
+ */
37
+ export const OWNERSHIP_HEADER = [
38
+ "This file is managed by Lisa and IS replaced on each `lisa` run.",
39
+ "Do not edit directly — durable changes belong upstream in Lisa.",
40
+ ];
41
+
42
+ /**
43
+ * Line comment syntax per extension, for the formats this generator emits.
44
+ *
45
+ * Deliberately a closed map rather than a default. A file type that is not
46
+ * listed makes `materialize` throw, so wiring a new generated asset through
47
+ * here is a decision someone has to make — the alternative is a silent
48
+ * pass-through that reintroduces the headerless copy this module exists to end.
49
+ */
50
+ const COMMENT_PREFIX = new Map([
51
+ [".sh", "#"],
52
+ [".bash", "#"],
53
+ [".mjs", "//"],
54
+ [".cjs", "//"],
55
+ [".js", "//"],
56
+ ]);
57
+
58
+ /**
59
+ * The comment prefix a generated file's format uses.
60
+ * @param {string} filePath - Path whose extension selects the syntax.
61
+ * @returns {string | undefined} Prefix, or undefined when unsupported.
62
+ */
63
+ export const commentPrefixFor = filePath =>
64
+ COMMENT_PREFIX.get(path.extname(filePath));
65
+
66
+ /**
67
+ * The header block as it appears in a file of the given comment syntax.
68
+ * @param {string} prefix - Line comment prefix, e.g. `#` or `//`.
69
+ * @returns {string[]} The header lines, ready to splice in.
70
+ */
71
+ const headerLines = prefix => OWNERSHIP_HEADER.map(line => `${prefix} ${line}`);
72
+
73
+ /**
74
+ * Whether contents already state the ownership contract.
75
+ * @param {string} contents - File contents.
76
+ * @returns {boolean} True when every header sentence is present.
77
+ */
78
+ export const hasOwnershipHeader = contents =>
79
+ OWNERSHIP_HEADER.every(line => contents.includes(line));
80
+
81
+ /**
82
+ * Contents with the ownership header stamped in.
83
+ *
84
+ * Placement is not cosmetic: a `#!` line that stops being line 1 stops being a
85
+ * shebang, so the header lands *below* it when one is present. Idempotent, so
86
+ * re-running the build over an already-stamped tree is a no-op rather than a
87
+ * pile of headers.
88
+ * @param {string} contents - Source file contents.
89
+ * @param {string} filePath - Destination path, for its extension.
90
+ * @returns {string} Contents carrying the header.
91
+ * @throws {Error} When the destination's format has no known comment syntax.
92
+ */
93
+ export const withOwnershipHeader = (contents, filePath) => {
94
+ const prefix = commentPrefixFor(filePath);
95
+ if (prefix === undefined) {
96
+ throw new Error(
97
+ `No comment syntax known for ${path.basename(filePath)} — a generated ` +
98
+ `copy-overwrite asset must be able to state its ownership contract.`
99
+ );
100
+ }
101
+ if (hasOwnershipHeader(contents)) return contents;
102
+
103
+ const lines = contents.split("\n");
104
+ const insertAt = lines[0]?.startsWith("#!") ? 1 : 0;
105
+ return [
106
+ ...lines.slice(0, insertAt),
107
+ ...headerLines(prefix),
108
+ "",
109
+ ...lines.slice(insertAt),
110
+ ].join("\n");
111
+ };
112
+
113
+ /**
114
+ * Contents with the ownership header removed, leaving the authored source.
115
+ *
116
+ * The byte-equality tests that pin a shipped copy to its reviewed original run
117
+ * through here, so "generated file minus its stamp" has exactly one definition
118
+ * and cannot drift from what the stamp step actually writes.
119
+ * @param {string} contents - Generated file contents.
120
+ * @param {string} filePath - Path, for its comment syntax.
121
+ * @returns {string} Contents without the header block.
122
+ */
123
+ export const withoutOwnershipHeader = (contents, filePath) => {
124
+ const prefix = commentPrefixFor(filePath);
125
+ if (prefix === undefined) return contents;
126
+
127
+ const block = headerLines(prefix);
128
+ const lines = contents.split("\n");
129
+ const at = lines.findIndex(
130
+ (line, index) => line === block[0] && lines[index + 1] === block[1]
131
+ );
132
+ if (at === -1) return contents;
133
+
134
+ // The blank separator belongs to the stamp, not to the source, so it comes
135
+ // out with it — otherwise round-tripping would grow a line each build.
136
+ const end =
137
+ lines[at + block.length] === "" ? at + block.length + 1 : at + block.length;
138
+ return [...lines.slice(0, at), ...lines.slice(end)].join("\n");
139
+ };
140
+
141
+ /**
142
+ * Read a source file and write it to a destination carrying the header.
143
+ * @param {string} source - Authored file to materialize.
144
+ * @param {string} destination - Path inside a `copy-overwrite/` tree.
145
+ * @returns {void}
146
+ */
147
+ export const materialize = (source, destination) => {
148
+ writeFileSync(
149
+ destination,
150
+ withOwnershipHeader(readFileSync(source, "utf8"), destination)
151
+ );
152
+ };
153
+
154
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
155
+ const [source, destination] = process.argv.slice(2);
156
+ if (!source || !destination) {
157
+ console.error(
158
+ "usage: materialize-copy-overwrite.mjs <source> <destination>"
159
+ );
160
+ process.exit(1);
161
+ }
162
+ materialize(source, destination);
163
+ }
@@ -1,3 +1,6 @@
1
+ <!-- This file is managed by Lisa and IS replaced on each `lisa` run. -->
2
+ <!-- Do not edit directly — durable changes belong upstream in Lisa. -->
3
+
1
4
  # GitHub Actions Configuration
2
5
 
3
6
  This directory contains the CI/CD workflows and automation for the project. This document explains how to configure and use the GitHub Actions workflows.
@@ -1,3 +1,6 @@
1
+ # This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ # Do not edit directly — durable changes belong upstream in Lisa.
3
+
1
4
  id: no-inline-component-in-container
2
5
  language: tsx
3
6
  severity: error
@@ -1,3 +1,6 @@
1
+ # This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ # Do not edit directly — durable changes belong upstream in Lisa.
3
+
1
4
  id: no-inline-component-in-view
2
5
  language: tsx
3
6
  severity: error
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
3
+ // Do not edit directly — durable changes belong upstream in Lisa.
4
+
2
5
  /**
3
6
  * Threshold ratchet gate — quality thresholds may tighten, never weaken.
4
7
  *
@@ -1,3 +1,6 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
1
4
  /**
2
5
  * Threshold ratchet — comparison rules and reporting.
3
6
  *
@@ -1,3 +1,6 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
1
4
  /**
2
5
  * Threshold ratchet — watched file families and value extractors.
3
6
  *