@esneiderbravo/speclaw 1.0.0 → 1.0.1

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.
package/README.md CHANGED
@@ -238,7 +238,10 @@ regenerate `ai-specs/` locally. Optional **`team.owners`** in
238
238
  **Enforcement artifacts.** For agents that support hooks, speclaw merges its law
239
239
  hooks into that agent's settings **by identity** — it never touches hooks you
240
240
  added yourself. The compiled law manifest lives in `.speclaw/laws-manifest.json`
241
- (gitignored), and a context-coverage log feeds `speclaw doctor`.
241
+ (gitignored) and is **adapted to the target tree** on `init`/`update` — speclaw's
242
+ own architecture laws are seeded only when those paths exist, and the cycle law
243
+ follows `apps/*/src`, `packages/*/src`, or `src/` rather than copying
244
+ `src/modules/**` from this package. A context-coverage log feeds `speclaw doctor`.
242
245
 
243
246
  <br/>
244
247
 
@@ -173,6 +173,17 @@ const MIGRATIONS = [
173
173
  "`npx @esneiderbravo/speclaw@latest init`. CI consumers use `esneiderbravo/speclaw@v1`.\n" +
174
174
  "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
175
175
  },
176
+ {
177
+ version: "1.0.1",
178
+ describe: "Seed laws match the target repository layout",
179
+ agentPrompt: "- After this update, speclaw rewrites `.speclaw/laws-manifest.json` from the " +
180
+ "target tree: dogfood laws (compass/foundation, ATTRIBUTION.md, local-first, " +
181
+ "protect-templates, shared-stays-inner) are dropped when those paths do not exist; " +
182
+ "the cycle law is scoped to detected source roots (`apps/*/src`, `packages/*/src`, " +
183
+ "`src/`, `lib/`) excluding test files, and considers import edges only. Run " +
184
+ "`speclaw laws compile` if agent rule files look stale, then `speclaw index`.\n" +
185
+ "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
186
+ },
176
187
  ];
177
188
  /**
178
189
  * Update speclaw and bring the current project up to date without a full re-init:
@@ -21,7 +21,8 @@
21
21
  "prose": "Justify any new dependency in the PR: it must not break 'runs offline with no API keys'.",
22
22
  "verification": { "kind": "path" },
23
23
  "enforcement": "feedback",
24
- "source": { "file": "LAWS.md" }
24
+ "source": { "file": "LAWS.md" },
25
+ "requires": ["src/modules/compass", "src/modules/foundation"]
25
26
  },
26
27
  {
27
28
  "id": "law~protect-templates~1",
@@ -32,7 +33,8 @@
32
33
  "prose": "Assets under src/modules/*/assets/** are product output — keep the {{placeholder}} and speclaw-init contracts intact, and let the build copy them (never hand-copy into dist/).",
33
34
  "verification": { "kind": "path" },
34
35
  "enforcement": "feedback",
35
- "source": { "file": "LAWS.md" }
36
+ "source": { "file": "LAWS.md" },
37
+ "requires": ["src/modules/foundation/assets"]
36
38
  },
37
39
  {
38
40
  "id": "law~honest-attribution~1",
@@ -43,7 +45,8 @@
43
45
  "prose": "Keep ATTRIBUTION.md accurate as the Compass and Lawbook modules evolve.",
44
46
  "verification": { "kind": "path" },
45
47
  "enforcement": "feedback",
46
- "source": { "file": "LAWS.md" }
48
+ "source": { "file": "LAWS.md" },
49
+ "requires": ["ATTRIBUTION.md"]
47
50
  },
48
51
  {
49
52
  "id": "law~shared-stays-inner~1",
@@ -62,7 +65,8 @@
62
65
  }
63
66
  },
64
67
  "enforcement": "gate",
65
- "source": { "file": "docs/standards/architecture.md" }
68
+ "source": { "file": "docs/standards/architecture.md" },
69
+ "requires": ["src/shared", "src/modules", "src/cli"]
66
70
  },
67
71
  {
68
72
  "id": "law~compass-does-not-import-foundation~1",
@@ -81,7 +85,8 @@
81
85
  }
82
86
  },
83
87
  "enforcement": "gate",
84
- "source": { "file": "docs/standards/architecture.md" }
88
+ "source": { "file": "docs/standards/architecture.md" },
89
+ "requires": ["src/modules/compass", "src/modules/foundation"]
85
90
  },
86
91
  {
87
92
  "id": "law~no-module-cycles~1",
@@ -90,9 +95,13 @@
90
95
  "severity": "error",
91
96
  "scope": ["src/modules/**"],
92
97
  "prose": "There are no circular dependencies between modules.",
93
- "verification": { "kind": "graph", "rule": { "circular": true } },
98
+ "verification": {
99
+ "kind": "graph",
100
+ "rule": { "circular": true, "edgeKinds": ["import"] }
101
+ },
94
102
  "enforcement": "gate",
95
- "source": { "file": "docs/standards/architecture.md" }
103
+ "source": { "file": "docs/standards/architecture.md" },
104
+ "adaptScope": "source-roots"
96
105
  }
97
106
  ]
98
107
  }
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { globError, isActiveLaw, readLawManifest, seedManifest, writeLawManifest, } from "./laws.js";
3
+ import { globError, isActiveLaw, readLawManifest, seedManifestFor, mergeSeedLaws, writeLawManifest, } from "./laws.js";
4
4
  import { parseLawsFromStandards } from "./laws-parse.js";
5
5
  import { agentsmdDialect, claudeRulesDialect, coderabbitDialect, copilotDialect, cursorMdcDialect, patchDelimited, } from "./dialects/index.js";
6
6
  import { detectConfiguredAgents } from "../../shared/agents.js";
@@ -17,6 +17,7 @@ const DIALECTS = [
17
17
  * Throws if standards declare duplicate ids.
18
18
  */
19
19
  export function mergeLawSources(projectPath) {
20
+ // Covers: req~adapt-seed-to-repo~1
20
21
  const parsed = parseLawsFromStandards(projectPath);
21
22
  if (parsed.duplicates.size > 0) {
22
23
  const detail = [...parsed.duplicates.entries()]
@@ -24,18 +25,15 @@ export function mergeLawSources(projectPath) {
24
25
  .join("; ");
25
26
  throw new Error(`duplicate law id(s) in docs/standards: ${detail}`);
26
27
  }
28
+ const disk = readLawManifest(projectPath);
29
+ const base = disk ? mergeSeedLaws(disk, projectPath).manifest : seedManifestFor(projectPath);
27
30
  const byId = new Map();
28
- for (const law of seedManifest().laws)
31
+ for (const law of base.laws)
29
32
  byId.set(law.id, law);
30
33
  for (const law of parsed.laws) {
31
34
  if (!byId.has(law.id))
32
35
  byId.set(law.id, law);
33
36
  }
34
- const disk = readLawManifest(projectPath);
35
- if (disk) {
36
- for (const law of disk.laws)
37
- byId.set(law.id, law);
38
- }
39
37
  return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
40
38
  }
41
39
  function validateScopes(laws) {
@@ -1,6 +1,7 @@
1
+ import { matchesScope } from "./laws.js";
1
2
  import { underPaths } from "./verify-model.js";
2
- /** Build the cross-file dependency graph, restricted to `paths` when given. */
3
- function buildGraph(db, paths, edgeKinds) {
3
+ /** Build the cross-file dependency graph, restricted to `paths` and `scope`. */
4
+ function buildGraph(db, paths, edgeKinds, scope) {
4
5
  const kindFilter = edgeKinds && edgeKinds.length > 0
5
6
  ? ` AND e.kind IN (${edgeKinds.map(() => "?").join(", ")})`
6
7
  : "";
@@ -16,6 +17,8 @@ function buildGraph(db, paths, edgeKinds) {
16
17
  for (const { src, dst } of rows) {
17
18
  if (!underPaths(src, paths) || !underPaths(dst, paths))
18
19
  continue;
20
+ if (!matchesScope(scope, src) || !matchesScope(scope, dst))
21
+ continue;
19
22
  const list = adj.get(src);
20
23
  if (list)
21
24
  list.push(dst);
@@ -205,8 +208,9 @@ function reachableFindings(law, rule, adj) {
205
208
  * resolved graph, so a graph law never reports an unknown here).
206
209
  */
207
210
  export function runGraphLaw(db, law, paths) {
211
+ // Covers: req~graph-honours-scope~1
208
212
  const rule = law.verification.rule;
209
- const adj = buildGraph(db, paths, rule.edgeKinds);
213
+ const adj = buildGraph(db, paths, rule.edgeKinds, law.scope);
210
214
  const findings = [];
211
215
  const wantReachable = rule.reachable === true && rule.from != null && rule.to != null;
212
216
  const wantCircular = rule.circular === true || (!rule.circular && !wantReachable);
@@ -1,13 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
- import { assetsDir } from "../../shared/paths.js";
5
- // The machine-readable law model and its manifest. This is the contract seam
6
- // (`.speclaw/laws-manifest.json`) between where laws come from and how they are
7
- // enforced: check-dispatcher owns the schema and the single `path` verification
8
- // backend; executable-laws extends the same model with `ast`/`deps`/`process`
9
- // backends by filling in more `verification.kind` cases — it never rewrites it.
10
- const ASSETS = assetsDir(import.meta.url);
4
+ import { adaptedLaws, catalogLaws, mergeAdaptedSeed } from "./seed-laws.js";
11
5
  const depsRuleSchema = z.object({
12
6
  name: z.string().optional(),
13
7
  from: z.string(),
@@ -144,46 +138,50 @@ export function writeLawManifest(projectPath, manifest) {
144
138
  fs.writeFileSync(p, JSON.stringify(validated, null, 2) + "\n");
145
139
  }
146
140
  /**
147
- * The starter law manifest shipped with speclaw, seeded from a speclaw-style
148
- * project's own `path`-verifiable Project-specific laws. It is the source the
149
- * MVP compiles into `.speclaw/laws-manifest.json`; once executable-laws lands,
150
- * laws are authored in `docs/standards/*` and compiled here instead. Laws whose
151
- * scope does not match a given repo are simply inert there.
141
+ * The full shipped catalog as persistable laws (adapter metadata stripped).
142
+ * Prefer {@link seedManifestFor} at init/verify time so consumer repos do not
143
+ * inherit speclaw-dogfood paths that do not exist there.
152
144
  *
153
- * @returns The validated seed manifest read from the module's assets.
154
- * @throws If the seed asset is missing or fails validation.
145
+ * @returns The validated catalog.
146
+ * @throws If the catalog fails the law schema.
155
147
  */
156
148
  export function seedManifest() {
157
- const raw = JSON.parse(fs.readFileSync(path.join(ASSETS, "laws", "laws-manifest.json"), "utf8"));
158
- return manifestSchema.parse(raw);
149
+ return manifestSchema.parse({ version: 1, laws: catalogLaws() });
150
+ }
151
+ /**
152
+ * Catalog laws adapted to `projectPath`: only laws whose `requires` paths exist,
153
+ * with the cycle law scoped to detected source roots.
154
+ *
155
+ * @param projectPath - Project root whose tree is inspected.
156
+ * @returns The validated adapted manifest.
157
+ */
158
+ export function seedManifestFor(projectPath) {
159
+ // Covers: req~adapt-seed-to-repo~1
160
+ return manifestSchema.parse({ version: 1, laws: adaptedLaws(projectPath) });
159
161
  }
160
162
  /**
161
163
  * The manifest the batch verifier should use: the project's file when present,
162
- * otherwise the shipped seed (so a clean CI clone does not silently pass).
164
+ * otherwise the **adapted** seed (so a clean CI clone does not silently pass,
165
+ * and does not evaluate inapplicable dogfood laws).
163
166
  *
164
167
  * @param projectPath - Project root to read from.
165
168
  */
166
169
  export function loadManifestForVerify(projectPath) {
167
- return readLawManifest(projectPath) ?? seedManifest();
170
+ return readLawManifest(projectPath) ?? seedManifestFor(projectPath);
168
171
  }
169
172
  /**
170
- * Append shipped seed laws whose `id` is not already in `existing`. Existing
171
- * entries are never overwritten a curated law keeps its prose, scope, and
172
- * enforcement across `update`.
173
+ * Merge an on-disk manifest with the adapted catalog. Existing curated entries
174
+ * (title or prose differs from the catalog) are never overwritten. Unmodified
175
+ * shipped laws whose required paths are absent are removed; unmodified cycle
176
+ * laws are replaced with the adapted scope.
173
177
  *
174
178
  * @param existing - The project's current manifest.
175
- * @returns The merged manifest and the ids that were added.
179
+ * @param projectPath - Project root used to adapt the catalog.
180
+ * @returns The merged manifest and the ids that were added or removed.
176
181
  */
177
- export function mergeSeedLaws(existing) {
178
- const seed = seedManifest();
179
- const have = new Set(existing.laws.map((l) => l.id));
180
- const extra = seed.laws.filter((l) => !have.has(l.id));
181
- if (extra.length === 0)
182
- return { manifest: existing, added: [] };
183
- return {
184
- manifest: { ...existing, laws: [...existing.laws, ...extra] },
185
- added: extra.map((l) => l.id),
186
- };
182
+ export function mergeSeedLaws(existing, projectPath) {
183
+ const { manifest, added, removed } = mergeAdaptedSeed({ version: existing.version, laws: existing.laws }, projectPath);
184
+ return { manifest: manifestSchema.parse(manifest), added, removed };
187
185
  }
188
186
  // ─── Glob matching (the `path` backend) ──────────────────────────────────────
189
187
  /**
@@ -8,7 +8,7 @@ import { installWorkflow } from "../lawbook/register.js";
8
8
  import { installPack, loadPacks } from "../tools/packs.js";
9
9
  import { readManifest, writeManifest } from "../../shared/manifest.js";
10
10
  import { pkgVersion } from "../../shared/version.js";
11
- import { mergeSeedLaws, readLawManifest, seedManifest, writeLawManifest, } from "./laws.js";
11
+ import { mergeSeedLaws, readLawManifest, seedManifestFor, writeLawManifest, } from "./laws.js";
12
12
  import { installHooks } from "./hooks.js";
13
13
  import { compileLaws } from "./compile-laws.js";
14
14
  import { refreshLockfile } from "./lock.js";
@@ -28,19 +28,22 @@ const FOUNDATION_DEFAULTS = {
28
28
  documentation_extra: "",
29
29
  };
30
30
  /**
31
- * Ensure the project has a law manifest. Missing → seed. Present append any
32
- * shipped seed law whose `id` is absent (never overwrite a curated entry).
31
+ * Ensure the project has a law manifest. Missing → adapted seed for this tree.
32
+ * Present merge the adapted catalog (never overwrite a curated entry; prune
33
+ * unmodified dogfood laws whose required paths are absent).
33
34
  */
34
35
  function ensureLawManifest(projectPath, report) {
35
36
  const existing = readLawManifest(projectPath);
36
37
  if (!existing) {
37
- const seed = seedManifest();
38
+ const seed = seedManifestFor(projectPath);
38
39
  writeLawManifest(projectPath, seed);
39
40
  report.written.push(path.join(projectPath, ".speclaw", "laws-manifest.json"));
40
41
  return seed;
41
42
  }
42
- const { manifest, added } = mergeSeedLaws(existing);
43
- if (added.length > 0) {
43
+ const { manifest, added, removed } = mergeSeedLaws(existing, projectPath);
44
+ if (added.length > 0 ||
45
+ removed.length > 0 ||
46
+ JSON.stringify(manifest.laws) !== JSON.stringify(existing.laws)) {
44
47
  writeLawManifest(projectPath, manifest);
45
48
  report.written.push(path.join(projectPath, ".speclaw", "laws-manifest.json"));
46
49
  }
@@ -0,0 +1,263 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { assetsDir } from "../../shared/paths.js";
4
+ // Catalog adapter: the shipped JSON is speclaw's own architecture plus a few
5
+ // portable laws. Consumer repos get only what their tree can actually host —
6
+ // never compass/foundation/ATTRIBUTION rules copied from this package.
7
+ //
8
+ // Types are declared here (not imported from laws.ts) so the adapter and the
9
+ // schema do not form a file-level import cycle.
10
+ const ASSETS = assetsDir(import.meta.url);
11
+ const CATALOG_PATH = path.join(ASSETS, "laws", "laws-manifest.json");
12
+ /** Globs that drop test doubles from the cycle-law graph. */
13
+ export const TEST_SCOPE_EXCLUSIONS = [
14
+ "!**/*.{spec,test}.{ts,tsx,js,jsx}",
15
+ "!**/test/**",
16
+ "!**/__tests__/**",
17
+ ];
18
+ let cachedCatalog = null;
19
+ /**
20
+ * Read the shipped law catalog (including adapter metadata). Cached for the
21
+ * process; tests that mutate the asset are not a supported path.
22
+ *
23
+ * @returns The parsed catalog.
24
+ * @throws If the asset is missing or not an object with a `laws` array.
25
+ */
26
+ export function readLawCatalog() {
27
+ if (cachedCatalog)
28
+ return cachedCatalog;
29
+ const raw = JSON.parse(fs.readFileSync(CATALOG_PATH, "utf8"));
30
+ if (!raw || !Array.isArray(raw.laws)) {
31
+ throw new Error(`invalid law catalog at ${CATALOG_PATH}`);
32
+ }
33
+ cachedCatalog = raw;
34
+ return raw;
35
+ }
36
+ /** Drop adapter-only fields so the result is a persistable law. */
37
+ export function asLaw(entry) {
38
+ const law = {
39
+ id: entry.id,
40
+ title: entry.title,
41
+ severity: entry.severity,
42
+ scope: [...entry.scope],
43
+ prose: entry.prose,
44
+ verification: structuredClone(entry.verification),
45
+ enforcement: entry.enforcement,
46
+ source: { ...entry.source },
47
+ };
48
+ if (entry.rationale !== undefined)
49
+ law.rationale = entry.rationale;
50
+ if (entry.status !== undefined)
51
+ law.status = entry.status;
52
+ return law;
53
+ }
54
+ /**
55
+ * Whether `req` names an existing file, directory, or glob under `projectPath`.
56
+ *
57
+ * @param projectPath - Project root.
58
+ * @param req - Relative path or glob (`*` / `**` in a segment).
59
+ */
60
+ export function pathRequirementExists(projectPath, req) {
61
+ const trimmed = req.replace(/^!/, "").replace(/\/\*\*$/, "");
62
+ return globExists(projectPath, trimmed.split("/").filter(Boolean));
63
+ }
64
+ function globExists(dir, parts) {
65
+ if (parts.length === 0)
66
+ return fs.existsSync(dir);
67
+ if (!fs.existsSync(dir))
68
+ return false;
69
+ const head = parts[0];
70
+ const rest = parts.slice(1);
71
+ if (head === "**") {
72
+ if (globExists(dir, rest))
73
+ return true;
74
+ let st;
75
+ try {
76
+ st = fs.statSync(dir);
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ if (!st.isDirectory())
82
+ return false;
83
+ for (const name of fs.readdirSync(dir)) {
84
+ if (name.startsWith("."))
85
+ continue;
86
+ if (globExists(path.join(dir, name), parts))
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ if (head === "*") {
92
+ let st;
93
+ try {
94
+ st = fs.statSync(dir);
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ if (!st.isDirectory())
100
+ return false;
101
+ for (const name of fs.readdirSync(dir)) {
102
+ if (name.startsWith("."))
103
+ continue;
104
+ if (globExists(path.join(dir, name), rest))
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ return globExists(path.join(dir, head), rest);
110
+ }
111
+ function childSrcExists(projectPath, parent) {
112
+ const dir = path.join(projectPath, parent);
113
+ try {
114
+ if (!fs.statSync(dir).isDirectory())
115
+ return false;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ for (const name of fs.readdirSync(dir)) {
121
+ if (name.startsWith("."))
122
+ continue;
123
+ try {
124
+ if (fs.statSync(path.join(dir, name, "src")).isDirectory())
125
+ return true;
126
+ }
127
+ catch {
128
+ /* skip */
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+ /**
134
+ * Source-root globs inferred from a repository layout.
135
+ *
136
+ * @param projectPath - Project root.
137
+ * @returns Globs covering app/package `src` trees, plus top-level `src/` or `lib/`, when they exist.
138
+ */
139
+ export function detectSourceRootGlobs(projectPath) {
140
+ const out = [];
141
+ if (childSrcExists(projectPath, "apps"))
142
+ out.push("apps/*/src/**");
143
+ if (childSrcExists(projectPath, "packages"))
144
+ out.push("packages/*/src/**");
145
+ if (pathRequirementExists(projectPath, "src"))
146
+ out.push("src/**");
147
+ if (pathRequirementExists(projectPath, "lib"))
148
+ out.push("lib/**");
149
+ return out;
150
+ }
151
+ function positivesSatisfied(projectPath, scope) {
152
+ const positives = scope.filter((g) => !g.startsWith("!"));
153
+ if (positives.length === 0)
154
+ return true;
155
+ return positives.some((g) => pathRequirementExists(projectPath, g));
156
+ }
157
+ function withTestExclusions(scope) {
158
+ const have = new Set(scope);
159
+ const out = [...scope];
160
+ for (const g of TEST_SCOPE_EXCLUSIONS) {
161
+ if (!have.has(g))
162
+ out.push(g);
163
+ }
164
+ return out;
165
+ }
166
+ function adaptCycleLaw(projectPath, entry) {
167
+ const law = asLaw(entry);
168
+ const catalogHits = positivesSatisfied(projectPath, entry.scope);
169
+ const roots = catalogHits
170
+ ? entry.scope.filter((g) => !g.startsWith("!"))
171
+ : detectSourceRootGlobs(projectPath);
172
+ if (roots.length === 0)
173
+ return null;
174
+ law.scope = withTestExclusions(roots);
175
+ if (law.verification.kind === "graph") {
176
+ law.verification = {
177
+ kind: "graph",
178
+ rule: { ...(law.verification.rule ?? {}), circular: true, edgeKinds: ["import"] },
179
+ };
180
+ }
181
+ return law;
182
+ }
183
+ function isApplicable(projectPath, entry) {
184
+ const reqs = entry.requires ?? [];
185
+ return reqs.every((r) => pathRequirementExists(projectPath, r));
186
+ }
187
+ /**
188
+ * Catalog laws stripped of adapter metadata — the full shipped set, unfiltered.
189
+ *
190
+ * @returns Persistable laws in catalog order.
191
+ */
192
+ export function catalogLaws() {
193
+ return readLawCatalog().laws.map(asLaw);
194
+ }
195
+ /**
196
+ * Laws from the catalog that apply to `projectPath`, with cycle-law scope
197
+ * rewritten to the repo's real source roots when needed.
198
+ *
199
+ * @param projectPath - Project root whose tree is inspected.
200
+ * @returns Persistable laws; adapter fields omitted.
201
+ */
202
+ export function adaptedLaws(projectPath) {
203
+ // Covers: req~adapt-seed-to-repo~1
204
+ const out = [];
205
+ for (const entry of readLawCatalog().laws) {
206
+ if (!isApplicable(projectPath, entry))
207
+ continue;
208
+ if (entry.adaptScope === "source-roots") {
209
+ const adapted = adaptCycleLaw(projectPath, entry);
210
+ if (adapted)
211
+ out.push(adapted);
212
+ continue;
213
+ }
214
+ out.push(asLaw(entry));
215
+ }
216
+ return out;
217
+ }
218
+ /** True when `existing` is still the shipped catalog text (title + prose). */
219
+ export function isUnmodifiedCatalogLaw(existing, catalog) {
220
+ return existing.title === catalog.title && existing.prose === catalog.prose;
221
+ }
222
+ /**
223
+ * Merge an on-disk manifest with the adapted catalog: append applicable ids,
224
+ * rewrite unmodified cycle-law scope, drop unmodified inapplicable dogfood.
225
+ *
226
+ * @param existing - The project's current manifest.
227
+ * @param projectPath - Project root used to adapt the catalog.
228
+ * @returns The merged manifest and the ids that were added or removed.
229
+ */
230
+ export function mergeAdaptedSeed(existing, projectPath) {
231
+ const catalog = readLawCatalog();
232
+ const byId = new Map(catalog.laws.map((l) => [l.id, l]));
233
+ const adapted = adaptedLaws(projectPath);
234
+ const adaptedById = new Map(adapted.map((l) => [l.id, l]));
235
+ const added = [];
236
+ const removed = [];
237
+ const result = [];
238
+ const seen = new Set();
239
+ for (const law of existing.laws) {
240
+ const cat = byId.get(law.id);
241
+ if (!cat || !isUnmodifiedCatalogLaw(law, cat)) {
242
+ result.push(law);
243
+ seen.add(law.id);
244
+ continue;
245
+ }
246
+ const next = adaptedById.get(law.id);
247
+ if (next) {
248
+ result.push(next);
249
+ seen.add(law.id);
250
+ }
251
+ else {
252
+ removed.push(law.id);
253
+ }
254
+ }
255
+ for (const law of adapted) {
256
+ if (seen.has(law.id))
257
+ continue;
258
+ result.push(law);
259
+ added.push(law.id);
260
+ seen.add(law.id);
261
+ }
262
+ return { manifest: { ...existing, laws: result }, added, removed };
263
+ }
@@ -17,8 +17,8 @@ export { underPaths } from "./verify-model.js";
17
17
  *
18
18
  * When the project has no index, every selected batch law is reported as
19
19
  * `skipped` with reason `no-index` (never silently passed). When the gitignored
20
- * manifest file is missing, the shipped seed is used so a clean clone does not
21
- * report an empty pass. Each evaluated law lands in exactly one of `passed` /
20
+ * manifest file is missing, the adapted seed is used so a clean clone does not
21
+ * report an empty pass or inherit inapplicable dogfood laws. Each evaluated law lands in exactly one of `passed` /
22
22
  * `failed` / `unknown`: it fails when the engine produced a finding, is
23
23
  * `unknown` when it produced none but rests on unresolved edges (which could
24
24
  * hide a violation), and passes otherwise.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },