@goodbones/core 0.1.0-beta.2 → 0.1.0-beta.4

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 (51) hide show
  1. package/build/dts/core/graph.d.ts +1 -0
  2. package/build/dts/core/graph.d.ts.map +1 -1
  3. package/build/dts/domain/manifest-location.d.ts +2 -0
  4. package/build/dts/domain/manifest-location.d.ts.map +1 -1
  5. package/build/dts/index.d.ts +6 -4
  6. package/build/dts/index.d.ts.map +1 -1
  7. package/build/dts/infrastructure/manifest-file.d.ts +1 -0
  8. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
  9. package/build/dts/infrastructure/manifest-include.d.ts +16 -0
  10. package/build/dts/infrastructure/manifest-include.d.ts.map +1 -0
  11. package/build/dts/infrastructure/walk.d.ts +6 -0
  12. package/build/dts/infrastructure/walk.d.ts.map +1 -1
  13. package/build/dts/manifest/infer.d.ts +55 -0
  14. package/build/dts/manifest/infer.d.ts.map +1 -0
  15. package/build/dts/manifest/json-schema.d.ts +2 -0
  16. package/build/dts/manifest/json-schema.d.ts.map +1 -1
  17. package/build/dts/manifest/manifest.d.ts +1 -1
  18. package/build/dts/manifest/manifest.d.ts.map +1 -1
  19. package/build/dts/ports/language.d.ts +2 -0
  20. package/build/dts/ports/language.d.ts.map +1 -1
  21. package/build/esm/core/graph.js +8 -0
  22. package/build/esm/core/graph.js.map +1 -1
  23. package/build/esm/domain/manifest-location.js +16 -1
  24. package/build/esm/domain/manifest-location.js.map +1 -1
  25. package/build/esm/index.js +6 -3
  26. package/build/esm/index.js.map +1 -1
  27. package/build/esm/infrastructure/manifest-file.js +31 -17
  28. package/build/esm/infrastructure/manifest-file.js.map +1 -1
  29. package/build/esm/infrastructure/manifest-include.js +187 -0
  30. package/build/esm/infrastructure/manifest-include.js.map +1 -0
  31. package/build/esm/infrastructure/walk.js +57 -1
  32. package/build/esm/infrastructure/walk.js.map +1 -1
  33. package/build/esm/manifest/infer.js +455 -0
  34. package/build/esm/manifest/infer.js.map +1 -0
  35. package/build/esm/manifest/json-schema.js +72 -17
  36. package/build/esm/manifest/json-schema.js.map +1 -1
  37. package/build/esm/manifest/manifest.js +7 -18
  38. package/build/esm/manifest/manifest.js.map +1 -1
  39. package/package.json +1 -1
  40. package/schema/architecture-node.schema.json +1518 -0
  41. package/schema/architecture.schema.json +1102 -691
  42. package/src/core/graph.ts +12 -0
  43. package/src/domain/manifest-location.ts +22 -0
  44. package/src/index.ts +33 -2
  45. package/src/infrastructure/manifest-file.ts +37 -17
  46. package/src/infrastructure/manifest-include.ts +318 -0
  47. package/src/infrastructure/walk.ts +70 -1
  48. package/src/manifest/infer.ts +643 -0
  49. package/src/manifest/json-schema.ts +83 -18
  50. package/src/manifest/manifest.ts +11 -20
  51. package/src/ports/language.ts +11 -0
@@ -7,13 +7,19 @@ import { Manifest } from "./manifest.js";
7
7
  // comment and a JSON file in a `$schema` key; either way the editor completes
8
8
  // keys and flags a misspelled one before the loader ever runs.
9
9
  //
10
- // Two things the codec does not know are added here: the `defs` map, and the
11
- // `{ use }` reference form that may stand in for any object — both belong to
12
- // the expansion pass that runs before decoding.
10
+ // Three things the codec does not know are added here: the `defs` map, the
11
+ // `{ use }` reference form that may stand in for any object, and the
12
+ // `{ include }` form that may stand in for any object or list — all belong to
13
+ // the passes that run before decoding.
13
14
 
14
15
  export const MANIFEST_SCHEMA_ID =
15
16
  "https://dataquail.github.io/goodbones/schema/architecture.schema.json";
16
17
 
18
+ // The schema an included file names: one node of the tree, with the two keys
19
+ // a file of its own may carry at the top.
20
+ export const MANIFEST_NODE_SCHEMA_ID =
21
+ "https://dataquail.github.io/goodbones/schema/architecture-node.schema.json";
22
+
17
23
  type JsonValue = string | number | boolean | null | JsonObject | ReadonlyArray<JsonValue>;
18
24
  type JsonObject = { readonly [key: string]: JsonValue };
19
25
 
@@ -23,11 +29,14 @@ const entriesOf = (value: JsonObject): ReadonlyArray<readonly [string, JsonValue
23
29
  const isObject = (value: JsonValue): value is JsonObject =>
24
30
  typeof value === "object" && value !== null && !Array.isArray(value);
25
31
 
32
+ const isList = (value: JsonValue): value is ReadonlyArray<JsonValue> => Array.isArray(value);
33
+
26
34
  // The generator names the recursive node after its own internal wrapper. A
27
35
  // stable name is what a `$ref` in an error message or a docs page can point at.
28
36
  const DEFINITION_NAMES: Readonly<Record<string, string>> = { Suspend_: "ManifestNode" };
29
37
 
30
38
  const USE_REFERENCE = "#/$defs/Use";
39
+ const INCLUDE_REFERENCE = "#/$defs/Include";
31
40
 
32
41
  const Use: JsonObject = {
33
42
  type: "object",
@@ -37,9 +46,20 @@ const Use: JsonObject = {
37
46
  required: ["use"],
38
47
  };
39
48
 
49
+ const Include: JsonObject = {
50
+ type: "object",
51
+ description:
52
+ "A reference to another YAML or JSON file, relative to this one. Replaced by that file's whole value before the manifest is decoded; a list item naming a file that holds a list is spliced in. Nothing may be written beside `include`.",
53
+ properties: { include: { type: "string" } },
54
+ required: ["include"],
55
+ additionalProperties: false,
56
+ };
57
+
40
58
  // Every object schema below the root becomes "this object, or a `use` of a
41
- // fragment shaped like it". The expansion pass replaces a reference wherever
42
- // it stands, so the schema admits one wherever an object may stand.
59
+ // fragment shaped like it, or an `include` of a file holding one", and every
60
+ // list schema "this list, or an `include` of a file holding one". The
61
+ // expansion passes replace a reference wherever it stands, so the schema
62
+ // admits one wherever the value may stand.
43
63
  const admitReferences = (value: JsonValue): JsonValue => {
44
64
  if (Array.isArray(value)) return value.map(admitReferences);
45
65
  if (!isObject(value)) return value;
@@ -53,8 +73,25 @@ const admitReferences = (value: JsonValue): JsonValue => {
53
73
  rebuilt[key] = admitReferences(entry);
54
74
  }
55
75
  }
56
- const isObjectSchema = rebuilt.type === "object" && "properties" in rebuilt;
57
- return isObjectSchema ? { anyOf: [{ $ref: USE_REFERENCE }, rebuilt] } : rebuilt;
76
+ if (rebuilt.type === "object" && "properties" in rebuilt) {
77
+ return { anyOf: [{ $ref: USE_REFERENCE }, { $ref: INCLUDE_REFERENCE }, rebuilt] };
78
+ }
79
+ if (rebuilt.type === "array") {
80
+ return { anyOf: [{ $ref: INCLUDE_REFERENCE }, rebuilt] };
81
+ }
82
+ return rebuilt;
83
+ };
84
+
85
+ const DEFS_PROPERTY: JsonObject = {
86
+ type: "object",
87
+ description:
88
+ 'Named fragments, referenced elsewhere in the manifest as `{ use: "<name>" }`. A fragment may itself contain `use`. Every file\'s `defs` share one namespace.',
89
+ additionalProperties: true,
90
+ };
91
+
92
+ const SCHEMA_PROPERTY: JsonObject = {
93
+ type: "string",
94
+ description: "For editors. Ignored by the loader.",
58
95
  };
59
96
 
60
97
  export const manifestJsonSchema = (): JsonObject => {
@@ -80,20 +117,48 @@ export const manifestJsonSchema = (): JsonObject => {
80
117
  "One manifest of a repository's architecture, read by @goodbones/cli and @goodbones/oxlint. See https://dataquail.github.io/goodbones/architecture-rules/manifest/.",
81
118
  ...root,
82
119
  properties: {
83
- $schema: {
84
- type: "string",
85
- description: "For editors. Ignored by the loader.",
86
- },
87
- defs: {
88
- type: "object",
89
- description:
90
- 'Named fragments, referenced elsewhere in the manifest as `{ use: "<name>" }`. A fragment may itself contain `use`.',
91
- additionalProperties: true,
92
- },
120
+ $schema: SCHEMA_PROPERTY,
121
+ defs: DEFS_PROPERTY,
93
122
  ...Object.fromEntries(
94
123
  entriesOf(properties).map(([key, value]) => [key, admitReferences(value)]),
95
124
  ),
96
125
  },
97
- $defs: { ...definitions, Use },
126
+ $defs: { ...definitions, Use, Include },
127
+ };
128
+ };
129
+
130
+ // One node of the tree as a file of its own — what a per-package
131
+ // `architecture.yaml` that the root manifest `include`s is shaped like. The
132
+ // node's object form, with the `$schema` and `defs` keys such a file may carry
133
+ // at the top, over the same definitions as the whole manifest.
134
+ export const manifestNodeJsonSchema = (): JsonObject => {
135
+ const whole = manifestJsonSchema();
136
+ const definitions = whole.$defs;
137
+ if (definitions === undefined || !isObject(definitions)) {
138
+ throw new Error("the manifest schema generated with no definitions");
139
+ }
140
+ const node = definitions.ManifestNode;
141
+ const variants = node !== undefined && isObject(node) ? node.anyOf : undefined;
142
+ const object =
143
+ variants !== undefined && isList(variants)
144
+ ? variants.find((variant) => isObject(variant) && variant.type === "object")
145
+ : undefined;
146
+ if (object === undefined || !isObject(object)) {
147
+ throw new Error("the manifest schema generated with no object form of a node");
148
+ }
149
+ const { $id: _id, $schema: _schema, ...rest } = object;
150
+ return {
151
+ $schema: "https://json-schema.org/draft/2020-12/schema",
152
+ $id: MANIFEST_NODE_SCHEMA_ID,
153
+ title: "Architecture manifest node",
154
+ description:
155
+ "One node of an architecture manifest's tree, as a file the manifest includes. See https://dataquail.github.io/goodbones/architecture-rules/manifest/#splitting-the-manifest-include.",
156
+ ...rest,
157
+ properties: {
158
+ $schema: SCHEMA_PROPERTY,
159
+ defs: DEFS_PROPERTY,
160
+ ...(rest.properties !== undefined && isObject(rest.properties) ? rest.properties : {}),
161
+ },
162
+ $defs: definitions,
98
163
  };
99
164
  };
@@ -4,7 +4,11 @@ import * as SchemaIssue from "effect/SchemaIssue";
4
4
 
5
5
  import { DeclarationKind, ResolveConfig } from "../domain/architecture-config.js";
6
6
  import { ConfigInvalid } from "../domain/architecture-error.js";
7
- import type { ManifestLocator, ManifestPath } from "../domain/manifest-location.js";
7
+ import {
8
+ type ManifestLocator,
9
+ type ManifestPath,
10
+ renderManifestPath,
11
+ } from "../domain/manifest-location.js";
8
12
  import { expandManifest, originOf, type Substitution } from "./expand.js";
9
13
 
10
14
  // A manifest is a tree of nodes keyed by path pattern, where everything the
@@ -406,32 +410,19 @@ export type DecodeManifestOptions = {
406
410
  readonly locate?: ManifestLocator | undefined;
407
411
  };
408
412
 
409
- const isIdentifier = (key: string): boolean => /^[A-Za-z_$][\w$]*$/.test(key);
410
-
411
- // `tree["~/core/"].members[0].subject` — dotted where a key reads as a name,
412
- // bracketed where it does not, so a node key that is a path pattern stays
413
- // legible.
414
- const renderPath = (path: ManifestPath): string =>
415
- path.length === 0
416
- ? "(root)"
417
- : path
418
- .map((segment, index) => {
419
- if (typeof segment === "number") return `[${String(segment)}]`;
420
- const key = String(segment);
421
- if (isIdentifier(key)) return index === 0 ? key : `.${key}`;
422
- return `[${JSON.stringify(key)}]`;
423
- })
424
- .join("");
425
-
426
413
  const fileLabelOf = (configPath: string): string => configPath.split(/[\\/]/).at(-1) ?? configPath;
427
414
 
415
+ // A position carries its own file when the value was written in a file the
416
+ // manifest `include`d; otherwise it is in the manifest file itself.
428
417
  const positionOf = (
429
418
  file: string,
430
419
  locate: ManifestLocator | undefined,
431
420
  path: ManifestPath,
432
421
  ): string | null => {
433
422
  const found = locate?.(path) ?? null;
434
- return found === null ? null : `${file}:${String(found.line)}:${String(found.column)}`;
423
+ return found === null
424
+ ? null
425
+ : `${found.file ?? file}:${String(found.line)}:${String(found.column)}`;
435
426
  };
436
427
 
437
428
  // One line per issue: where in the file, which path, what was wrong — and,
@@ -452,7 +443,7 @@ const describeIssue = (
452
443
  return `via \`use: ${JSON.stringify(name)}\`${position === null ? "" : ` at ${position}`}`;
453
444
  });
454
445
  return (
455
- ` ${at === null ? "" : `${at} `}${renderPath(origin.path)}: ${detail}` +
446
+ ` ${at === null ? "" : `${at} `}${renderManifestPath(origin.path)}: ${detail}` +
456
447
  (via.length === 0 ? "" : ` (${via.join(", ")})`)
457
448
  );
458
449
  };
@@ -21,6 +21,17 @@ export type Language = {
21
21
  // Files carrying one of those extensions that are not source — for
22
22
  // TypeScript, a declaration file states types and no linter visits one.
23
23
  readonly ignoredFiles: ReadonlyArray<RegExp>;
24
+ // The files whose presence makes a folder a package of this language —
25
+ // `package.json`, `go.mod`, `Cargo.toml`. `infer` counts its depth from a
26
+ // package rather than from the repository root, so a monorepo of twenty
27
+ // packages is described one package at a time. Empty if the ecosystem has no
28
+ // such marker.
29
+ readonly packageMarkers: ReadonlyArray<string>;
30
+ // The folder names a package of this language keeps its source under, in
31
+ // order of preference — `src` for TypeScript, nothing for Go, where source
32
+ // sits at the package root. The first one that exists is where `infer`
33
+ // starts counting.
34
+ readonly sourceRoots: ReadonlyArray<string>;
24
35
  // Reads the facts out of one source text. The CLI reads every file through
25
36
  // it, and a probe carrying a `source` snippet is parsed by it at load.
26
37
  readonly extractor: FactExtractor;