@goodbones/oxlint 0.1.0-beta.2 → 0.1.0-beta.21

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 (52) hide show
  1. package/build/dts/campaigns/host.d.ts +19 -0
  2. package/build/dts/campaigns/host.d.ts.map +1 -0
  3. package/build/dts/campaigns/live.d.ts +3 -0
  4. package/build/dts/campaigns/live.d.ts.map +1 -0
  5. package/build/dts/campaigns/none.d.ts +6 -0
  6. package/build/dts/campaigns/none.d.ts.map +1 -0
  7. package/build/dts/campaigns/rule.d.ts +6 -0
  8. package/build/dts/campaigns/rule.d.ts.map +1 -0
  9. package/build/dts/config-loader.d.ts +3 -3
  10. package/build/dts/config-loader.d.ts.map +1 -1
  11. package/build/dts/exports-rule.d.ts.map +1 -1
  12. package/build/dts/imports-rule.d.ts.map +1 -1
  13. package/build/dts/members-rule.d.ts.map +1 -1
  14. package/build/dts/oxlint-api.d.ts +4 -33
  15. package/build/dts/oxlint-api.d.ts.map +1 -1
  16. package/build/dts/plugin.d.ts +1 -0
  17. package/build/dts/plugin.d.ts.map +1 -1
  18. package/build/dts/surface-rule.d.ts.map +1 -1
  19. package/build/esm/campaigns/host.js +2 -0
  20. package/build/esm/campaigns/host.js.map +1 -0
  21. package/build/esm/campaigns/live.js +81 -0
  22. package/build/esm/campaigns/live.js.map +1 -0
  23. package/build/esm/campaigns/none.js +46 -0
  24. package/build/esm/campaigns/none.js.map +1 -0
  25. package/build/esm/campaigns/rule.js +163 -0
  26. package/build/esm/campaigns/rule.js.map +1 -0
  27. package/build/esm/config-loader.js +37 -5
  28. package/build/esm/config-loader.js.map +1 -1
  29. package/build/esm/exports-rule.js +15 -71
  30. package/build/esm/exports-rule.js.map +1 -1
  31. package/build/esm/imports-rule.js +14 -26
  32. package/build/esm/imports-rule.js.map +1 -1
  33. package/build/esm/members-rule.js +10 -112
  34. package/build/esm/members-rule.js.map +1 -1
  35. package/build/esm/oxlint-api.js +22 -20
  36. package/build/esm/oxlint-api.js.map +1 -1
  37. package/build/esm/plugin.js +4 -0
  38. package/build/esm/plugin.js.map +1 -1
  39. package/build/esm/surface-rule.js +11 -127
  40. package/build/esm/surface-rule.js.map +1 -1
  41. package/package.json +19 -7
  42. package/src/campaigns/host.ts +45 -0
  43. package/src/campaigns/live.ts +106 -0
  44. package/src/campaigns/none.ts +54 -0
  45. package/src/campaigns/rule.ts +243 -0
  46. package/src/config-loader.ts +42 -5
  47. package/src/exports-rule.ts +19 -106
  48. package/src/imports-rule.ts +16 -35
  49. package/src/members-rule.ts +11 -165
  50. package/src/oxlint-api.ts +31 -47
  51. package/src/plugin.ts +5 -0
  52. package/src/surface-rule.ts +15 -189
@@ -1 +1 @@
1
- {"version":3,"file":"surface-rule.js","sourceRoot":"","sources":["../../src/surface-rule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface-rule.js","sourceRoot":"","sources":["../../src/surface-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,EACf,aAAa,EAEb,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,EAAE,EACF,cAAc,EAId,cAAc,GACf,MAAM,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAc,EAAE,CAAC,CAAC;IACpE,IAAI,EAAE;QACJ,IAAI,EAAE,SAAkB;QACxB,IAAI,EAAE;YACJ,WAAW,EACT,iHAAiH;SACpH;QACD,MAAM,EAAE,EAAE;KACX;IAED,UAAU,CAAC,OAAoB;QAC7B,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,QAAQ,GAAuC,EAAE,CAAC;QAEtD,OAAO;YACL,MAAM;gBACJ,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACzD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,KAAK,CAAC;gBACxC,QAAQ,GAAG,qBAAqB,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;gBAC5D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,CAAC;YAED,OAAO,CAAC,IAAa;gBACnB,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC;gBACrD,MAAM,UAAU,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;gBAE1D,uEAAuE;gBACvE,sCAAsC;gBACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;gBACvC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC;wBAAE,SAAS;oBACrD,MAAM,IAAI,GACR,SAAS,CAAC,OAAO,KAAK,IAAI;wBACxB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC5E,IAAI,IAAI,KAAK,SAAS;wBAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACvC,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;wBAC/C,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC;qBAClC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@goodbones/oxlint",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "description": "The oxlint plugin for @goodbones/core: the imports, exports, members, structure and surface families as five oxlint rules, evaluated in the editor and in CI from the same manifest the CLI reads.",
6
+ "description": "The oxlint plugin for @goodbones/core: the imports, exports, members, structure, surface and campaigns families as six oxlint rules, evaluated in the editor and in CI from the same manifest the CLI reads.",
7
7
  "author": "zacharyweidenbach",
8
8
  "keywords": [
9
9
  "architecture",
@@ -43,16 +43,28 @@
43
43
  "!src/**/*.test.ts"
44
44
  ],
45
45
  "dependencies": {
46
- "effect": "4.0.0-beta.94",
47
- "@goodbones/typescript": "0.1.0-beta.2",
48
- "@goodbones/core": "0.1.0-beta.2"
46
+ "effect": "4.0.5",
47
+ "@goodbones/core": "0.1.0-beta.18",
48
+ "@goodbones/typescript": "0.1.0-beta.19"
49
49
  },
50
50
  "peerDependencies": {
51
- "oxlint": ">=1.77.0"
51
+ "oxlint": ">=1.77.0",
52
+ "@goodbones/ast-grep": "^0.1.3-beta.7",
53
+ "@goodbones/campaigns": "^0.1.1-beta.9"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@goodbones/ast-grep": {
57
+ "optional": true
58
+ },
59
+ "@goodbones/campaigns": {
60
+ "optional": true
61
+ }
52
62
  },
53
63
  "devDependencies": {
54
64
  "@types/node": "^25.9.4",
55
- "oxlint": "1.81.0"
65
+ "oxlint": "1.81.0",
66
+ "@goodbones/campaigns": "0.1.1-beta.9",
67
+ "@goodbones/ast-grep": "0.1.3-beta.7"
56
68
  },
57
69
  "nx": {
58
70
  "includedScripts": []
@@ -0,0 +1,45 @@
1
+ // What the plugin asks of the campaigns family, as one value.
2
+ //
3
+ // `@goodbones/campaigns` is an optional peer: a user who never runs a
4
+ // campaign does not install it. So nothing outside this folder names it, and
5
+ // nothing here reaches it at run time — this module and `none.ts` import its
6
+ // types only, which compile away. `live.ts` is the one module that loads it,
7
+ // and the composition root reaches `live.ts` through `importOptional`, so a
8
+ // repository without the package lints with every other rule.
9
+ import type {
10
+ LoadedPolicy as CorePolicy,
11
+ PolicyExtension,
12
+ UninstalledExtension,
13
+ } from "@goodbones/core";
14
+
15
+ import type { OxlintRule } from "../oxlint-api.js";
16
+
17
+ export type CampaignsHost = {
18
+ // The `campaigns` rule over the loaded policy.
19
+ readonly rule: (policy: CorePolicy) => OxlintRule;
20
+ };
21
+
22
+ // The policy as this host carries it: the core's, with the campaigns family
23
+ // it was composed with — the live one, or `none`.
24
+ export type LoadedPolicy = CorePolicy & { readonly campaigns: CampaignsHost };
25
+
26
+ // What the composition root hands the loader for the family: the manifest
27
+ // with each `fn` term's function imported and replaced by its name, the
28
+ // extension that claims the family's keys — or, with the family not
29
+ // installed, those keys named as uninstalled.
30
+ export type Composition = {
31
+ readonly manifest: unknown;
32
+ readonly extensions: ReadonlyArray<PolicyExtension>;
33
+ readonly uninstalled: ReadonlyArray<UninstalledExtension>;
34
+ };
35
+
36
+ export type CampaignsFamily = {
37
+ readonly host: CampaignsHost;
38
+ readonly compose: (
39
+ repoRoot: string,
40
+ configPath: string,
41
+ manifest: unknown,
42
+ ) => Promise<Composition>;
43
+ // Run once the policy has loaded and before oxlint lints a file.
44
+ readonly prepare: (policy: CorePolicy) => Promise<void>;
45
+ };
@@ -0,0 +1,106 @@
1
+ import * as path from "node:path";
2
+
3
+ import {
4
+ campaignsExtension,
5
+ campaignsOf,
6
+ discoverSectors,
7
+ loadCampaignFunctions,
8
+ makeReportSourceLive,
9
+ reportSpecsOf,
10
+ type SectorIndex,
11
+ } from "@goodbones/campaigns";
12
+ import {
13
+ globToRegExp,
14
+ listSourceFiles,
15
+ listWorkspaceProjects,
16
+ type LoadedPolicy,
17
+ ReportUnavailable,
18
+ } from "@goodbones/core";
19
+
20
+ import type { CampaignsFamily } from "./host.js";
21
+ import { makeCampaignsRule } from "./rule.js";
22
+
23
+ // The campaigns family as the plugin runs it, over `@goodbones/campaigns`.
24
+ // The one module that loads the package: the composition root imports it
25
+ // through `importOptional`, so its absence is `none`, not a failure to load.
26
+
27
+ // A `marker` or `nx` perimeter needs the other files to say which sector a
28
+ // file is in — the markers, or the workspace's projects — so the plugin
29
+ // reads them once at load, from a walk of the repository that reads no
30
+ // source text but the markers'. The other perimeters answer from the path
31
+ // or the file itself, and need nothing here.
32
+ const discoverSectorIndexes = (policy: LoadedPolicy): ReadonlyMap<string, SectorIndex> => {
33
+ const indexes = new Map<string, SectorIndex>();
34
+ const needing = campaignsOf(policy).campaignRules.filter(
35
+ (rule) => rule.perimeter?.kind === "marker" || rule.perimeter?.kind === "nx",
36
+ );
37
+ if (needing.length === 0) return indexes;
38
+ const widened = [...new Set(needing.flatMap((rule) => rule.extensions))];
39
+ const files = listSourceFiles(policy.repoRoot, ["."], policy.languages, widened);
40
+ const known = new Set(policy.languages.flatMap((one) => one.extensions));
41
+ const projects = needing.some((rule) => rule.perimeter?.kind === "nx")
42
+ ? listWorkspaceProjects(policy.repoRoot, ["."])
43
+ : [];
44
+ for (const rule of needing) {
45
+ const scoped = files.filter(
46
+ (file) =>
47
+ rule.scope.some((pattern) => pattern.test(file)) &&
48
+ (known.has(path.extname(file)) || rule.extensions.includes(path.extname(file))),
49
+ );
50
+ indexes.set(
51
+ rule.id,
52
+ discoverSectors(rule, {
53
+ files: scoped,
54
+ readText: (file) => policy.fileSystem.readText(file),
55
+ globToRegExp,
56
+ projects,
57
+ }),
58
+ );
59
+ }
60
+ return indexes;
61
+ };
62
+
63
+ export const liveCampaigns: CampaignsFamily = {
64
+ // The sectors a `marker` or `nx` perimeter births are read once here,
65
+ // from the markers and the workspace, before any file is linted.
66
+ host: { rule: (policy) => makeCampaignsRule(policy, discoverSectorIndexes(policy)) },
67
+ // The `fn` terms are imported here, before the policy loads, as the CLI
68
+ // does. A `report` command runs once per process — once per editor session
69
+ // for oxlint's language server, which then sees that report until it
70
+ // restarts. A report the build writes to a file is the predictable form.
71
+ compose: async (repoRoot, configPath, manifest) => {
72
+ const loaded = await loadCampaignFunctions(configPath, manifest);
73
+ return {
74
+ manifest: loaded.manifest,
75
+ extensions: [
76
+ campaignsExtension({
77
+ functions: loaded.functions,
78
+ reports: makeReportSourceLive(repoRoot),
79
+ }),
80
+ ],
81
+ uninstalled: [],
82
+ };
83
+ },
84
+ // Every `report` a campaign names is read now, while oxlint has linted
85
+ // nothing. A `command` forks this process, and once the linter is running
86
+ // Linux can refuse the fork: the linter's per-thread AST buffers merge
87
+ // into one mapping larger than RAM and swap together, which the default
88
+ // overcommit heuristic refuses to duplicate. The source keeps the answer,
89
+ // and a failure, so the rules read what was read here — a term's several
90
+ // commands run at once. A report that cannot be read is not a load
91
+ // failure — the campaigns rule reports it once, on the first file a
92
+ // campaign naming it selects; one that does not parse is.
93
+ prepare: async (policy) => {
94
+ const campaigns = campaignsOf(policy);
95
+ await Promise.all(
96
+ reportSpecsOf(campaigns.campaignRules).map(async (spec) => {
97
+ if (campaigns.reports.read !== undefined) return campaigns.reports.read(spec);
98
+ try {
99
+ campaigns.reports.diagnosticsOf(spec, "");
100
+ } catch (cause) {
101
+ if (!(cause instanceof ReportUnavailable)) throw cause;
102
+ }
103
+ }),
104
+ );
105
+ },
106
+ };
@@ -0,0 +1,54 @@
1
+ import type { UninstalledExtension } from "@goodbones/core";
2
+
3
+ import type { OxlintRule, Program, RuleContext } from "../oxlint-api.js";
4
+ import type { CampaignsFamily } from "./host.js";
5
+
6
+ // The package the family comes in, and the manifest keys it would claim: a
7
+ // manifest that uses one without it is refused naming the package, rather
8
+ // than as a misspelling.
9
+ export const CAMPAIGNS_PACKAGE = "@goodbones/campaigns";
10
+
11
+ const UNINSTALLED: UninstalledExtension = {
12
+ manifestKeys: ["campaigns", "ledger"],
13
+ install: CAMPAIGNS_PACKAGE,
14
+ };
15
+
16
+ // The `campaigns` rule without the family. oxlint refuses a configuration
17
+ // naming a rule the plugin does not register, with no word of why, so the
18
+ // rule is registered anyway and says what to install — once per process, on
19
+ // the first file, rather than on every file of the run.
20
+ export const notInstalledRule = (): OxlintRule => {
21
+ let said = false;
22
+ return {
23
+ meta: {
24
+ type: "problem" as const,
25
+ docs: { description: `the campaigns, which need ${CAMPAIGNS_PACKAGE} installed` },
26
+ schema: [],
27
+ },
28
+ createOnce(context: RuleContext) {
29
+ return {
30
+ before() {
31
+ return !said;
32
+ },
33
+ Program(_node: Program) {
34
+ if (said) return;
35
+ said = true;
36
+ context.report({
37
+ message:
38
+ `architecture/campaigns needs \`${CAMPAIGNS_PACKAGE}\`, which is not installed. ` +
39
+ `Install it beside @goodbones/oxlint, or turn the rule off.`,
40
+ loc: { line: 1, column: 0 },
41
+ });
42
+ },
43
+ };
44
+ },
45
+ };
46
+ };
47
+
48
+ // The family, when it is not installed.
49
+ export const campaignsNotInstalled: CampaignsFamily = {
50
+ host: { rule: notInstalledRule },
51
+ compose: (_repoRoot, _configPath, manifest) =>
52
+ Promise.resolve({ manifest, extensions: [], uninstalled: [UNINSTALLED] }),
53
+ prepare: () => Promise.resolve(),
54
+ };
@@ -0,0 +1,243 @@
1
+ import * as path from "node:path";
2
+
3
+ import {
4
+ type CampaignHit,
5
+ type CampaignInput,
6
+ campaignsOf,
7
+ campaignsSelecting,
8
+ type CompiledCampaign,
9
+ type CompiledObjective,
10
+ detectorCandidatesOf,
11
+ detectorOf,
12
+ evaluateObjectives,
13
+ isShared,
14
+ ledgerKeyOf,
15
+ ledgerPhaseOf,
16
+ membershipOf,
17
+ needsSyntax,
18
+ objectivesInWindow,
19
+ positionOf,
20
+ reconcileSector,
21
+ sectorEntryOf,
22
+ type SectorIndex,
23
+ sharedWindow,
24
+ } from "@goodbones/campaigns";
25
+ import { formatMessage, type LoadedPolicy, ReportUnavailable } from "@goodbones/core";
26
+ import { sourceFactsOf } from "@goodbones/typescript";
27
+
28
+ import {
29
+ factsOfProgram,
30
+ type OxlintRule,
31
+ type Program,
32
+ type RuleContext,
33
+ toRepoRelative,
34
+ } from "../oxlint-api.js";
35
+
36
+ // The campaigns family, one file at a time: every hit of an objective in
37
+ // window for the file's sector that its ledger does not carry, reported at
38
+ // the match's position with the objective's `how` as the message. The
39
+ // facts come from oxlint's tree through the pack's reader, as the other
40
+ // rules read them; the `syntax` term is answered by the same matcher the
41
+ // CLI uses, over `sourceCode.text` — "one engine" is this family's parity
42
+ // contract, rather than a corpus.
43
+ //
44
+ // The sector comes from the perimeter on this file (a marker or nx index
45
+ // the host built at load, for the two forms that need the other files),
46
+ // and the sector's phase from the ledgers — which objectives carry
47
+ // holdouts for it, and its record's `reached` and attestations — never
48
+ // from a walk. So the plugin's answer is the code as of the last `clear`,
49
+ // and the CLI's is the code now; between a fix and its `clear` they
50
+ // differ, and `check` fails on the stale holdout until they agree. A
51
+ // `has` objective and an end state are never reported here: their holdout
52
+ // is the sector, and they reach this file only as the phase they put it at.
53
+
54
+ // The sectors a host discovered at load, per campaign, for the perimeters
55
+ // that need the other files.
56
+ export type SectorIndexes = ReadonlyMap<string, SectorIndex>;
57
+
58
+ const unledgered = (
59
+ policy: LoadedPolicy,
60
+ rule: CompiledCampaign,
61
+ sector: string,
62
+ root: string,
63
+ hits: ReadonlyArray<CampaignHit>,
64
+ ): ReadonlyArray<CampaignHit> => {
65
+ const carried = new Set<CampaignHit>();
66
+ const byObjective = new Map<string, Array<CampaignHit>>();
67
+ for (const hit of hits) {
68
+ byObjective.set(hit.objective, [...(byObjective.get(hit.objective) ?? []), hit]);
69
+ }
70
+ for (const [objectiveId, own] of byObjective) {
71
+ const objective = rule.objectives.find((one) => one.id === objectiveId);
72
+ const ledger = campaignsOf(policy).ledgers.get(ledgerKeyOf(rule.id, objectiveId));
73
+ if (objective === undefined || ledger === undefined) continue;
74
+ const entries = own.map((hit) => sectorEntryOf(hit.violation, root));
75
+ const known = new Set(reconcileSector(ledger, sector, entries, objective.unit).ledgered);
76
+ own.forEach((hit, i) => {
77
+ if (known.has(entries[i] ?? "")) carried.add(hit);
78
+ });
79
+ }
80
+ return hits.filter((hit) => !carried.has(hit));
81
+ };
82
+
83
+ // The objectives in window for the sector, read off the ledgers: the phase
84
+ // they place it at (a sector no `clear` has placed stands at the first, as
85
+ // the legacy does), with a prerequisite counted by what the shared files'
86
+ // ledger carries. On the shared files themselves, which stand on no phase, it is
87
+ // what is read over it. A prerequisite is never reported on a sector's own
88
+ // file — its holdouts are the shared files'.
89
+ const inWindowFor = (
90
+ policy: LoadedPolicy,
91
+ rule: CompiledCampaign,
92
+ sector: string,
93
+ ): ReadonlyArray<CompiledObjective> => {
94
+ const window = isShared(rule, sector)
95
+ ? sharedWindow(rule)
96
+ : objectivesInWindow(
97
+ rule,
98
+ ledgerPhaseOf(policy, rule, sector),
99
+ positionOf(rule, campaignsOf(policy).sectorRecords.get(ledgerKeyOf(rule.id, sector))),
100
+ ).filter((one) => !one.overShared);
101
+ return window.filter((one) => one.detect !== null);
102
+ };
103
+
104
+ // A `report` a campaign names that cannot be read — a command the kernel
105
+ // refused to spawn, a file no step wrote — is one fact about the run, not
106
+ // one about each file the campaign selects. The live source keeps the
107
+ // failure per spec; this keeps which failures have been said, so the first
108
+ // selected file carries the notice and the rest stay quiet. Without it, a
109
+ // lint of eight hundred files is eight hundred copies of oxlint's generic
110
+ // "error running JS plugin", with the cause dropped by the terser formats.
111
+ const aside = (cause: ReportUnavailable): string =>
112
+ `A report a campaign names could not be read, so no campaign naming it is judged in this ` +
113
+ `run: ${cause.message}`;
114
+
115
+ export const makeCampaignsRule = (
116
+ policy: LoadedPolicy,
117
+ indexes: SectorIndexes = new Map(),
118
+ ): OxlintRule => {
119
+ // Per rule instance, which is per plugin load — one process — rather than
120
+ // per `createOnce`, which a host may call more often than that.
121
+ const said = new Set<string>();
122
+ const known = new Set(policy.languages.flatMap((one) => one.extensions));
123
+ return {
124
+ meta: {
125
+ type: "problem" as const,
126
+ docs: {
127
+ description:
128
+ "the campaigns: every place a pattern the repository is migrating away from still occurs, in a sector whose phase asks for it, that its ledger does not carry",
129
+ },
130
+ schema: [],
131
+ },
132
+
133
+ createOnce(context: RuleContext) {
134
+ let file = "";
135
+ let selected: ReadonlyArray<CompiledCampaign> = [];
136
+
137
+ return {
138
+ before() {
139
+ file = toRepoRelative(policy.repoRoot, context.filename);
140
+ if (file.startsWith("..")) return false;
141
+ // A file the packs would not walk — one oxlint lints anyway — is
142
+ // seen only by a campaign that widened its scope to it, so both
143
+ // hosts answer about the same files.
144
+ const extension = path.extname(file);
145
+ selected = campaignsSelecting(campaignsOf(policy).campaignRules, file).filter(
146
+ (rule) =>
147
+ known.size === 0 || known.has(extension) || rule.extensions.includes(extension),
148
+ );
149
+ return selected.length > 0;
150
+ },
151
+
152
+ Program(node: Program) {
153
+ const text = context.sourceCode.text;
154
+ const detectors = selected.flatMap((rule) => [
155
+ ...rule.objectives.flatMap((one) => {
156
+ const detect = detectorOf(one);
157
+ return detect === null ? [] : [detect];
158
+ }),
159
+ ...(rule.perimeter?.kind === "match" ? [rule.perimeter.detect] : []),
160
+ ]);
161
+ const input: CampaignInput = {
162
+ file,
163
+ text,
164
+ facts: sourceFactsOf(factsOfProgram(file, node)),
165
+ resolver: policy.resolver,
166
+ fileSystem: policy.fileSystem,
167
+ syntax: needsSyntax(detectors) ? policy.syntax.parse(file, text) : null,
168
+ functions: campaignsOf(policy).functions,
169
+ reports: campaignsOf(policy).reports,
170
+ };
171
+ // One campaign at a time, so a report one of them cannot read costs
172
+ // that campaign's answer for this file and not its neighbours'.
173
+ const unrecorded: Array<CampaignHit> = [];
174
+ for (const rule of selected) {
175
+ try {
176
+ const perimeter = rule.perimeter;
177
+ const anchors =
178
+ perimeter?.kind === "match"
179
+ ? detectorCandidatesOf(perimeter.detect, perimeter.unit, input).map(
180
+ (one) => one.key.split("#")[0] ?? one.key,
181
+ )
182
+ : [];
183
+ const place = membershipOf(rule, file, indexes.get(rule.id) ?? null, anchors);
184
+ // Hits grouped by the sector they fall in, then judged
185
+ // against that sector's window and ledger.
186
+ const bySector = new Map<string, { root: string; hits: Array<CampaignHit> }>();
187
+ const windows = new Map<string, ReadonlySet<string>>();
188
+ const placedAt = (subject: string | null) => {
189
+ const placement = place(subject);
190
+ if (placement === null) return null;
191
+ if (!windows.has(placement.sector)) {
192
+ windows.set(
193
+ placement.sector,
194
+ new Set(inWindowFor(policy, rule, placement.sector).map((one) => one.id)),
195
+ );
196
+ }
197
+ return placement;
198
+ };
199
+ const whole = placedAt(null);
200
+ const candidates =
201
+ whole === null
202
+ ? rule.objectives.filter((one) => one.detect !== null)
203
+ : rule.objectives.filter(
204
+ (one) =>
205
+ one.detect !== null && (windows.get(whole.sector)?.has(one.id) ?? false),
206
+ );
207
+ if (perimeter?.kind !== "match" && whole === null) continue;
208
+ for (const hit of evaluateObjectives(candidates, input)) {
209
+ const placement = placedAt(hit.violation.subject);
210
+ if (placement === null) continue;
211
+ if (!(windows.get(placement.sector)?.has(hit.objective) ?? false)) continue;
212
+ const group = bySector.get(placement.sector) ?? { root: placement.root, hits: [] };
213
+ group.hits.push(hit);
214
+ bySector.set(placement.sector, group);
215
+ }
216
+ for (const [sector, group] of bySector) {
217
+ for (const hit of unledgered(policy, rule, sector, group.root, group.hits)) {
218
+ unrecorded.push(hit);
219
+ }
220
+ }
221
+ } catch (cause) {
222
+ if (!(cause instanceof ReportUnavailable)) throw cause;
223
+ const message = aside(cause);
224
+ if (said.has(message)) continue;
225
+ said.add(message);
226
+ context.report({ message, loc: { line: 1, column: 0 } });
227
+ }
228
+ }
229
+
230
+ for (const hit of unrecorded) {
231
+ // A match lands where it was found; a file-unit hit, on line 1, as
232
+ // the structure rule places a finding about the file itself.
233
+ const at = hit.range ?? { start: { line: 0, column: 0 } };
234
+ context.report({
235
+ message: formatMessage(hit.violation),
236
+ loc: { line: at.start.line + 1, column: at.start.column },
237
+ });
238
+ }
239
+ },
240
+ };
241
+ },
242
+ };
243
+ };
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
 
3
3
  import {
4
4
  findManifestFile,
5
- type LoadedPolicy,
5
+ importOptional,
6
6
  loadPolicy,
7
7
  makeFileSystemLive,
8
8
  readManifestFile,
@@ -10,16 +10,42 @@ import {
10
10
  import { typescriptLanguage } from "@goodbones/typescript";
11
11
  import * as Result from "effect/Result";
12
12
 
13
- export type { LoadedPolicy } from "@goodbones/core";
13
+ import type { CampaignsFamily, LoadedPolicy } from "./campaigns/host.js";
14
+ import { campaignsNotInstalled } from "./campaigns/none.js";
15
+
16
+ export type { LoadedPolicy } from "./campaigns/host.js";
17
+
18
+ // The campaigns family, when `@goodbones/campaigns` is installed beside the
19
+ // plugin, and `none` when it is not. Everything that names the package is
20
+ // under `campaigns/`, reached through this one import.
21
+ const hostCampaigns = async (): Promise<CampaignsFamily> =>
22
+ (await importOptional(() => import("./campaigns/live.js"), "@goodbones/campaigns"))
23
+ ?.liveCampaigns ?? campaignsNotInstalled;
24
+
25
+ // The clock the campaigns are judged by; `ARCHITECTURE_NOW` pins it, as it
26
+ // does for the CLI.
27
+ const hostNow = (): number => {
28
+ const pinned = process.env.ARCHITECTURE_NOW;
29
+ if (pinned === undefined || pinned === "") return Date.now();
30
+ const parsed = /^\d+$/.test(pinned) ? Number(pinned) : Date.parse(pinned);
31
+ if (Number.isNaN(parsed)) {
32
+ throw new Error(`ARCHITECTURE_NOW is ${JSON.stringify(pinned)}, which is not a date.`);
33
+ }
34
+ return parsed;
35
+ };
14
36
 
15
37
  // The plugin's composition root: read the manifest file, construct the
16
38
  // language packs and the live file system, and hand them to the loader. A load
17
39
  // failure throws out of here and out of oxlint's import of the plugin — a
18
40
  // plugin that came up with no policy would report nothing and be
19
41
  // indistinguishable from a clean codebase.
42
+ //
43
+ // `given` is the family to compose: found by `hostCampaigns` unless a test
44
+ // names one, which is how the plugin is run as if the package were absent.
20
45
  export const loadPolicyFromFile = async (
21
46
  repoRoot: string,
22
47
  configFilename?: string,
48
+ given?: CampaignsFamily,
23
49
  ): Promise<LoadedPolicy> => {
24
50
  // Named, or discovered: architecture.yaml, .yml, .json, or .config.mjs,
25
51
  // exactly one of which may be present.
@@ -28,14 +54,25 @@ export const loadPolicyFromFile = async (
28
54
  ? findManifestFile(repoRoot)
29
55
  : path.resolve(repoRoot, configFilename);
30
56
  const read = await readManifestFile(configPath);
57
+ const campaigns = given ?? (await hostCampaigns());
58
+ const composed = await campaigns.compose(repoRoot, configPath, read.manifest);
59
+ // The syntax matcher, for the campaigns family's `syntax` term, is an
60
+ // optional peer with a native binary: composed into the pack when it is
61
+ // installed, and the plugin parses with it for that family only. A
62
+ // campaign that needs it without it is refused at load, naming it.
63
+ const astGrep = await importOptional(() => import("@goodbones/ast-grep"), "@goodbones/ast-grep");
31
64
  const loaded = loadPolicy({
32
65
  repoRoot,
33
66
  configPath,
34
- manifest: read.manifest,
67
+ manifest: composed.manifest,
35
68
  locate: read.locate,
36
- languages: [typescriptLanguage()],
69
+ languages: [typescriptLanguage(astGrep === null ? {} : { syntax: astGrep.astGrepMatcher() })],
37
70
  fileSystem: makeFileSystemLive(repoRoot),
71
+ extensions: composed.extensions,
72
+ uninstalled: composed.uninstalled,
73
+ now: hostNow(),
38
74
  });
39
75
  if (Result.isFailure(loaded)) throw loaded.failure;
40
- return loaded.success;
76
+ await campaigns.prepare(loaded.success);
77
+ return { ...loaded.success, campaigns: campaigns.host };
41
78
  };