@goodbones/core 0.1.0-beta.1 → 0.1.0-beta.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 (37) hide show
  1. package/build/dts/domain/manifest-location.d.ts +7 -0
  2. package/build/dts/domain/manifest-location.d.ts.map +1 -0
  3. package/build/dts/index.d.ts +5 -2
  4. package/build/dts/index.d.ts.map +1 -1
  5. package/build/dts/infrastructure/manifest-file.d.ts +10 -2
  6. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
  7. package/build/dts/load/policy.d.ts +2 -0
  8. package/build/dts/load/policy.d.ts.map +1 -1
  9. package/build/dts/manifest/expand.d.ts +26 -0
  10. package/build/dts/manifest/expand.d.ts.map +1 -0
  11. package/build/dts/manifest/json-schema.d.ts +8 -0
  12. package/build/dts/manifest/json-schema.d.ts.map +1 -0
  13. package/build/dts/manifest/manifest.d.ts +5 -1
  14. package/build/dts/manifest/manifest.d.ts.map +1 -1
  15. package/build/esm/domain/manifest-location.js +6 -0
  16. package/build/esm/domain/manifest-location.js.map +1 -0
  17. package/build/esm/index.js +3 -1
  18. package/build/esm/index.js.map +1 -1
  19. package/build/esm/infrastructure/manifest-file.js +144 -7
  20. package/build/esm/infrastructure/manifest-file.js.map +1 -1
  21. package/build/esm/load/policy.js +1 -1
  22. package/build/esm/load/policy.js.map +1 -1
  23. package/build/esm/manifest/expand.js +111 -0
  24. package/build/esm/manifest/expand.js.map +1 -0
  25. package/build/esm/manifest/json-schema.js +76 -0
  26. package/build/esm/manifest/json-schema.js.map +1 -0
  27. package/build/esm/manifest/manifest.js +69 -4
  28. package/build/esm/manifest/manifest.js.map +1 -1
  29. package/package.json +7 -3
  30. package/schema/architecture.schema.json +1089 -0
  31. package/src/domain/manifest-location.ts +19 -0
  32. package/src/index.ts +22 -1
  33. package/src/infrastructure/manifest-file.ts +184 -8
  34. package/src/load/policy.ts +5 -1
  35. package/src/manifest/expand.ts +174 -0
  36. package/src/manifest/json-schema.ts +99 -0
  37. package/src/manifest/manifest.ts +110 -9
@@ -1,8 +1,11 @@
1
1
  import * as Result from "effect/Result";
2
2
  import * as Schema from "effect/Schema";
3
+ import * as SchemaIssue from "effect/SchemaIssue";
3
4
 
4
5
  import { DeclarationKind, ResolveConfig } from "../domain/architecture-config.js";
5
6
  import { ConfigInvalid } from "../domain/architecture-error.js";
7
+ import type { ManifestLocator, ManifestPath } from "../domain/manifest-location.js";
8
+ import { expandManifest, originOf, type Substitution } from "./expand.js";
6
9
 
7
10
  // A manifest is a tree of nodes keyed by path pattern, where everything the
8
11
  // architecture says about a part of the tree is written at that part of the tree.
@@ -286,7 +289,12 @@ export type ExportRestriction = typeof ExportRestriction.Type;
286
289
  export const globsOf = (globs: string | ReadonlyArray<string>): ReadonlyArray<string> =>
287
290
  typeof globs === "string" ? [globs] : globs;
288
291
 
289
- const decode = Schema.decodeUnknownResult(Manifest);
292
+ // Every issue, not the first: a manifest is edited by hand, and the reader
293
+ // fixing one line wants to know about the other three. A key the schema does
294
+ // not declare is refused rather than dropped — a misspelled `matchNot` that
295
+ // decoded to nothing would be a rule quietly enforcing less than it says.
296
+ const decode = Schema.decodeUnknownResult(Manifest, { errors: "all", onExcessProperty: "error" });
297
+ const flatten = SchemaIssue.makeFormatterStandardSchemaV1();
290
298
 
291
299
  export type DecodedManifest = {
292
300
  readonly manifest: Manifest;
@@ -392,17 +400,110 @@ const normalizeLegacyMembers = (
392
400
  return { input: { ...input, tree }, notices };
393
401
  };
394
402
 
403
+ export type DecodeManifestOptions = {
404
+ // Turns a path in the file into a line and column. The YAML reader supplies
405
+ // one; a JavaScript module has no positions to give and passes nothing.
406
+ readonly locate?: ManifestLocator | undefined;
407
+ };
408
+
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
+ const fileLabelOf = (configPath: string): string => configPath.split(/[\\/]/).at(-1) ?? configPath;
427
+
428
+ const positionOf = (
429
+ file: string,
430
+ locate: ManifestLocator | undefined,
431
+ path: ManifestPath,
432
+ ): string | null => {
433
+ const found = locate?.(path) ?? null;
434
+ return found === null ? null : `${file}:${String(found.line)}:${String(found.column)}`;
435
+ };
436
+
437
+ // One line per issue: where in the file, which path, what was wrong — and,
438
+ // when the value came in through a `use`, the reference that pulled it in,
439
+ // since the fragment's own line may sit far from where the reader is looking.
440
+ const describeIssue = (
441
+ configPath: string,
442
+ locate: ManifestLocator | undefined,
443
+ substitutions: ReadonlyArray<Substitution>,
444
+ path: ManifestPath,
445
+ detail: string,
446
+ ): string => {
447
+ const file = fileLabelOf(configPath);
448
+ const origin = originOf(substitutions, path);
449
+ const at = positionOf(file, locate, origin.path);
450
+ const via = origin.via.map(({ at: ref, name }) => {
451
+ const position = positionOf(file, locate, ref);
452
+ return `via \`use: ${JSON.stringify(name)}\`${position === null ? "" : ` at ${position}`}`;
453
+ });
454
+ return (
455
+ ` ${at === null ? "" : `${at} `}${renderPath(origin.path)}: ${detail}` +
456
+ (via.length === 0 ? "" : ` (${via.join(", ")})`)
457
+ );
458
+ };
459
+
460
+ // The standard-schema formatter flattens the issue tree to `{ path, message }`
461
+ // pairs; a path segment may arrive wrapped as `{ key }`.
462
+ const pathOf = (issue: {
463
+ readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
464
+ }): ManifestPath =>
465
+ (issue.path ?? []).map((segment) => (typeof segment === "object" ? segment.key : segment));
466
+
395
467
  export const decodeManifest = (
396
468
  configPath: string,
397
469
  input: unknown,
470
+ options: DecodeManifestOptions = {},
398
471
  ): Result.Result<DecodedManifest, ConfigInvalid> => {
399
- const resolve = normalizeLegacyResolve(input);
472
+ const expanded = expandManifest(input);
473
+ if (Result.isFailure(expanded)) {
474
+ return Result.fail(
475
+ new ConfigInvalid({
476
+ configPath,
477
+ detail:
478
+ "the manifest does not expand:\n" +
479
+ describeIssue(
480
+ configPath,
481
+ options.locate,
482
+ [],
483
+ expanded.failure.path,
484
+ expanded.failure.detail,
485
+ ),
486
+ }),
487
+ );
488
+ }
489
+ const { substitutions, value } = expanded.success;
490
+
491
+ const resolve = normalizeLegacyResolve(value);
400
492
  const members = normalizeLegacyMembers(resolve.input);
401
- return Result.map(
402
- Result.mapError(
403
- decode(members.input),
404
- (issue) => new ConfigInvalid({ configPath, detail: String(issue) }),
405
- ),
406
- (manifest) => ({ manifest, notices: [...resolve.notices, ...members.notices] }),
407
- );
493
+ const decoded = decode(members.input);
494
+ if (Result.isFailure(decoded)) {
495
+ const lines = flatten(decoded.failure.issue).issues.map((issue) =>
496
+ describeIssue(configPath, options.locate, substitutions, pathOf(issue), issue.message),
497
+ );
498
+ return Result.fail(
499
+ new ConfigInvalid({
500
+ configPath,
501
+ detail: `the manifest does not decode:\n${lines.join("\n")}`,
502
+ }),
503
+ );
504
+ }
505
+ return Result.succeed({
506
+ manifest: decoded.success,
507
+ notices: [...resolve.notices, ...members.notices],
508
+ });
408
509
  };