repo-contract 0.7.0 → 0.7.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 (92) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -0
  3. package/bin/templates.mjs +4 -4
  4. package/dist/.dts/config/define-repo-contract.d.ts +1 -0
  5. package/dist/.dts/config/define-repo-contract.d.ts.map +1 -1
  6. package/dist/.dts/errors.d.ts +21 -2
  7. package/dist/.dts/errors.d.ts.map +1 -1
  8. package/dist/.dts/helpers/exception-policy.d.ts +22 -2
  9. package/dist/.dts/helpers/exception-policy.d.ts.map +1 -1
  10. package/dist/.dts/helpers/load-exception-registry.d.ts +1 -0
  11. package/dist/.dts/helpers/load-exception-registry.d.ts.map +1 -1
  12. package/dist/.dts/helpers/reconcile-exceptions.d.ts +7 -1
  13. package/dist/.dts/helpers/reconcile-exceptions.d.ts.map +1 -1
  14. package/dist/.dts/helpers/write-exception-registry.d.ts +1 -0
  15. package/dist/.dts/helpers/write-exception-registry.d.ts.map +1 -1
  16. package/dist/.dts/policy/run-policies.d.ts.map +1 -1
  17. package/dist/.dts/presets/arethetypeswrong.d.ts +4 -1
  18. package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -1
  19. package/dist/.dts/presets/broken-links.d.ts +1 -0
  20. package/dist/.dts/presets/broken-links.d.ts.map +1 -1
  21. package/dist/.dts/presets/commitlint.d.ts +1 -0
  22. package/dist/.dts/presets/commitlint.d.ts.map +1 -1
  23. package/dist/.dts/presets/dead-code.d.ts +1 -0
  24. package/dist/.dts/presets/dead-code.d.ts.map +1 -1
  25. package/dist/.dts/presets/duplication.d.ts +1 -0
  26. package/dist/.dts/presets/duplication.d.ts.map +1 -1
  27. package/dist/.dts/presets/e2e.d.ts +4 -1
  28. package/dist/.dts/presets/e2e.d.ts.map +1 -1
  29. package/dist/.dts/presets/format.d.ts +4 -1
  30. package/dist/.dts/presets/format.d.ts.map +1 -1
  31. package/dist/.dts/presets/license.d.ts +4 -1
  32. package/dist/.dts/presets/license.d.ts.map +1 -1
  33. package/dist/.dts/presets/lint.d.ts +1 -0
  34. package/dist/.dts/presets/lint.d.ts.map +1 -1
  35. package/dist/.dts/presets/markdownlint.d.ts +1 -0
  36. package/dist/.dts/presets/markdownlint.d.ts.map +1 -1
  37. package/dist/.dts/presets/publint.d.ts +1 -0
  38. package/dist/.dts/presets/publint.d.ts.map +1 -1
  39. package/dist/.dts/presets/security-deps.d.ts +4 -1
  40. package/dist/.dts/presets/security-deps.d.ts.map +1 -1
  41. package/dist/.dts/presets/security-secrets.d.ts +4 -1
  42. package/dist/.dts/presets/security-secrets.d.ts.map +1 -1
  43. package/dist/.dts/presets/stylelint.d.ts +1 -0
  44. package/dist/.dts/presets/stylelint.d.ts.map +1 -1
  45. package/dist/.dts/presets/test.d.ts +4 -1
  46. package/dist/.dts/presets/test.d.ts.map +1 -1
  47. package/dist/.dts/presets/typecheck.d.ts +4 -1
  48. package/dist/.dts/presets/typecheck.d.ts.map +1 -1
  49. package/dist/.dts/run-repo-contract.d.ts +1 -0
  50. package/dist/.dts/run-repo-contract.d.ts.map +1 -1
  51. package/dist/.dts/standard-schema/types.d.ts +12 -1
  52. package/dist/.dts/standard-schema/types.d.ts.map +1 -1
  53. package/dist/.dts/types.d.ts +48 -9
  54. package/dist/.dts/types.d.ts.map +1 -1
  55. package/dist/helpers.cjs.map +1 -1
  56. package/dist/helpers.js.map +1 -1
  57. package/dist/index.cjs +18 -0
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.js +18 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/presets.cjs +15 -4
  62. package/dist/presets.cjs.map +1 -1
  63. package/dist/presets.js +15 -4
  64. package/dist/presets.js.map +1 -1
  65. package/package.json +3 -3
  66. package/schemas/verdict.schema.json +1 -1
  67. package/src/config/define-repo-contract.ts +1 -0
  68. package/src/errors.ts +21 -2
  69. package/src/helpers/exception-policy.ts +22 -2
  70. package/src/helpers/load-exception-registry.ts +1 -0
  71. package/src/helpers/reconcile-exceptions.ts +7 -1
  72. package/src/helpers/write-exception-registry.ts +1 -0
  73. package/src/policy/run-policies.ts +65 -6
  74. package/src/presets/arethetypeswrong.ts +4 -1
  75. package/src/presets/broken-links.ts +1 -0
  76. package/src/presets/commitlint.ts +1 -0
  77. package/src/presets/dead-code.ts +1 -0
  78. package/src/presets/duplication.ts +1 -0
  79. package/src/presets/e2e.ts +4 -1
  80. package/src/presets/format.ts +4 -1
  81. package/src/presets/license.ts +4 -1
  82. package/src/presets/lint.ts +1 -0
  83. package/src/presets/markdownlint.ts +1 -0
  84. package/src/presets/publint.ts +1 -0
  85. package/src/presets/security-deps.ts +4 -1
  86. package/src/presets/security-secrets.ts +4 -1
  87. package/src/presets/stylelint.ts +1 -0
  88. package/src/presets/test.ts +36 -6
  89. package/src/presets/typecheck.ts +4 -1
  90. package/src/run-repo-contract.ts +1 -0
  91. package/src/standard-schema/types.ts +12 -1
  92. package/src/types.ts +48 -9
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/presets/test.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAKxD,oFAAoF;AACpF,eAAO,MAAM,IAAI,EAAE,qBAYlB,CAAA"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/presets/test.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,aAAa,CAAA;AAgBtE;;;GAGG;AACH,eAAO,MAAM,IAAI,EAAE,qBA2BlB,CAAA"}
@@ -1,4 +1,7 @@
1
1
  import type { CheckDefinitionConfig } from "../types.js";
2
- /** Type checking via `tsc --noEmit`. */
2
+ /**
3
+ * Type checking via `tsc --noEmit`.
4
+ * @beta
5
+ */
3
6
  export declare const typecheck: CheckDefinitionConfig;
4
7
  //# sourceMappingURL=typecheck.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typecheck.d.ts","sourceRoot":"","sources":["../../../src/presets/typecheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAKxD,wCAAwC;AACxC,eAAO,MAAM,SAAS,EAAE,qBAkBvB,CAAA"}
1
+ {"version":3,"file":"typecheck.d.ts","sourceRoot":"","sources":["../../../src/presets/typecheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAKxD;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,qBAkBvB,CAAA"}
@@ -30,6 +30,7 @@ import type { CheckSchema, Evidence, RepoContractConfig, RunRepoContractOptions,
30
30
  * @param config - the repo-contract configuration to run: its checks, concurrency, and their policies
31
31
  * @param options - run options; `options.checks` restricts execution to specific check ids, `options.signal` allows cancelling the run
32
32
  * @returns the assembled `evidence` for every check together with the aggregated `verdict`
33
+ * @public
33
34
  */
34
35
  export declare function runRepoContract<const TChecks extends CheckSchema>(config: RepoContractConfig<TChecks>, options?: RunRepoContractOptions): Promise<{
35
36
  evidence: Evidence<TChecks>;
@@ -1 +1 @@
1
- {"version":3,"file":"run-repo-contract.d.ts","sourceRoot":"","sources":["../../src/run-repo-contract.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,sBAAsB,EACtB,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,OAAO,SAAS,WAAW,EAC/D,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,EACnC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAGrE"}
1
+ {"version":3,"file":"run-repo-contract.d.ts","sourceRoot":"","sources":["../../src/run-repo-contract.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,sBAAsB,EACtB,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,OAAO,SAAS,WAAW,EAC/D,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,EACnC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAGrE"}
@@ -12,12 +12,23 @@
12
12
  * interface, since repo-contract has no use for the shared base on its own -- structurally
13
13
  * identical for any real schema object assigned to it. Re-diff against
14
14
  * `@standard-schema/spec`'s published `dist/index.d.ts` if this file is ever touched.
15
+ *
16
+ * Tagged `@public`, not tied to either re-exporting barrel's own stability tier (this type is
17
+ * re-exported from both the Stable root barrel, for `output.schema`, and the Experimental
18
+ * `repo-contract/helpers` barrel): this vendored copy's own shape only ever changes via a
19
+ * deliberate re-diff against the pinned upstream spec version above, never as a side effect of
20
+ * either barrel's own stability classification.
21
+ * @public
15
22
  */
16
23
  export interface StandardSchemaV1<Input = unknown, Output = Input> {
17
24
  /** The Standard Schema properties. */
18
25
  readonly "~standard": StandardSchemaV1.Props<Input, Output>;
19
26
  }
20
- /** Namespaced members of {@link StandardSchemaV1}: `Props`, `Result`, `SuccessResult`, `FailureResult`, `Issue`, `PathSegment`, `Types`, `InferInput`, `InferOutput`. */
27
+ /**
28
+ * Namespaced members of `StandardSchemaV1`: `Props`, `Result`, `SuccessResult`, `FailureResult`,
29
+ * `Issue`, `PathSegment`, `Types`, `InferInput`, `InferOutput`.
30
+ * @public
31
+ */
21
32
  export declare namespace StandardSchemaV1 {
22
33
  /** The Standard Schema properties interface. */
23
34
  interface Props<Input = unknown, Output = Input> {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/standard-schema/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;IAC/D,sCAAsC;IACtC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC5D;AAED,yKAAyK;AAEzK,MAAM,CAAC,OAAO,WAAW,gBAAgB,CAAC;IACxC,gDAAgD;IAChD,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACpD,0CAA0C;QAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;QACnB,6CAA6C;QAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,sCAAsC;QACtC,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,OAAO,KACd,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7C,iDAAiD;QACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;KAClD;IAED,sCAAsC;IACtC,UAAiB,OAAO;QACtB,6EAA6E;QAC7E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;KAC9D;IAED,qDAAqD;IACrD,KAAY,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,CAAA;IAElE,mDAAmD;IACnD,UAAiB,aAAa,CAAC,MAAM;QACnC,8BAA8B;QAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;QACtB,oDAAoD;QACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAC5B;IAED,gDAAgD;IAChD,UAAiB,aAAa;QAC5B,uCAAuC;QACvC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;KAClC;IAED,iDAAiD;IACjD,UAAiB,KAAK;QACpB,sCAAsC;QACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;QACxB,qCAAqC;QACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,WAAW,CAAC,EAAE,GAAG,SAAS,CAAA;KACnE;IAED,+CAA+C;IAC/C,UAAiB,WAAW;QAC1B,2CAA2C;QAC3C,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;KAC1B;IAED,2CAA2C;IAC3C,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACpD,oCAAoC;QACpC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;QACrB,qCAAqC;QACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KACxB;IAED,kDAAkD;IAClD,KAAY,UAAU,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACnE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC7B,CAAC,OAAO,CAAC,CAAA;IAEV,mDAAmD;IACnD,KAAY,WAAW,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACpE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC7B,CAAC,QAAQ,CAAC,CAAA;CACZ"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/standard-schema/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;IAC/D,sCAAsC;IACtC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC5D;AAED;;;;GAIG;AAEH,MAAM,CAAC,OAAO,WAAW,gBAAgB,CAAC;IACxC,gDAAgD;IAChD,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACpD,0CAA0C;QAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;QACnB,6CAA6C;QAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,sCAAsC;QACtC,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,OAAO,KACd,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7C,iDAAiD;QACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;KAClD;IAED,sCAAsC;IACtC,UAAiB,OAAO;QACtB,6EAA6E;QAC7E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;KAC9D;IAED,qDAAqD;IACrD,KAAY,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,CAAA;IAElE,mDAAmD;IACnD,UAAiB,aAAa,CAAC,MAAM;QACnC,8BAA8B;QAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;QACtB,oDAAoD;QACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAC5B;IAED,gDAAgD;IAChD,UAAiB,aAAa;QAC5B,uCAAuC;QACvC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;KAClC;IAED,iDAAiD;IACjD,UAAiB,KAAK;QACpB,sCAAsC;QACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;QACxB,qCAAqC;QACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,WAAW,CAAC,EAAE,GAAG,SAAS,CAAA;KACnE;IAED,+CAA+C;IAC/C,UAAiB,WAAW;QAC1B,2CAA2C;QAC3C,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;KAC1B;IAED,2CAA2C;IAC3C,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACpD,oCAAoC;QACpC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;QACrB,qCAAqC;QACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KACxB;IAED,kDAAkD;IAClD,KAAY,UAAU,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACnE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC7B,CAAC,OAAO,CAAC,CAAA;IAEV,mDAAmD;IACnD,KAAY,WAAW,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACpE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC7B,CAAC,QAAQ,CAAC,CAAA;CACZ"}
@@ -13,6 +13,7 @@ import type { StandardSchemaV1 } from "./standard-schema/types.js";
13
13
  * consumers actually run; a check whose tool emits another format converts it in its own `run`
14
14
  * step or reads it directly in its `policy` function, using whatever library it already depends
15
15
  * on, rather than this package carrying an optional peer dependency on everyone's behalf.
16
+ * @public
16
17
  */
17
18
  export type OutputFormat = "json" | "text";
18
19
  /**
@@ -29,9 +30,13 @@ export type OutputFormat = "json" | "text";
29
30
  * `"host_terminated"`: repo-contract did request that signal, just not via `options.signal` or
30
31
  * `timeoutMs` (see `"aborted"`/`"timed_out"`), so it must not be conflated with an externally-caused
31
32
  * `"signaled"`.
33
+ * @public
32
34
  */
33
35
  export type CheckStatus = "completed" | "timed_out" | "signaled" | "host_terminated" | "spawn_error" | "aborted";
34
- /** A requested parse of a check's stdout succeeded. */
36
+ /**
37
+ * A requested parse of a check's stdout succeeded.
38
+ * @public
39
+ */
35
40
  export interface ParsedOutputSuccess<T> {
36
41
  /** The format that was requested and successfully parsed. */
37
42
  readonly format: OutputFormat;
@@ -44,6 +49,7 @@ export interface ParsedOutputSuccess<T> {
44
49
  * A requested parse of a check's stdout failed. The raw stdout on the
45
50
  * parent `CheckEvidence` is preserved unchanged -- a parse failure is never
46
51
  * silently reinterpreted or discarded.
52
+ * @public
47
53
  */
48
54
  export interface ParsedOutputFailure {
49
55
  /** The format that was requested (and failed to parse). */
@@ -53,7 +59,10 @@ export interface ParsedOutputFailure {
53
59
  /** The parse error's message. */
54
60
  readonly error: string;
55
61
  }
56
- /** The result of a check's requested output-format parse: either a successful `ParsedOutputSuccess`, or a `ParsedOutputFailure`. */
62
+ /**
63
+ * The result of a check's requested output-format parse: either a successful `ParsedOutputSuccess`, or a `ParsedOutputFailure`.
64
+ * @public
65
+ */
57
66
  export type ParsedOutput<T> = ParsedOutputSuccess<T> | ParsedOutputFailure;
58
67
  /**
59
68
  * What actually happened when one configured check ran. `output` is present
@@ -76,6 +85,7 @@ export type ParsedOutput<T> = ParsedOutputSuccess<T> | ParsedOutputFailure;
76
85
  * typing via `StandardSchemaV1<Input, Output>` for their own code, just not threaded through this
77
86
  * shared `CheckEvidence` shape. A policy author narrows or casts `.value` themselves, exactly as
78
87
  * they already must for `"json"`/`"text"` with no schema supplied.
88
+ * @public
79
89
  */
80
90
  export interface CheckEvidence {
81
91
  /** The executable that was actually spawned (after tokenization, if `run` was a string). */
@@ -116,6 +126,7 @@ export interface CheckEvidence {
116
126
  * Says nothing about whether any of it was acceptable; see `Verdict`.
117
127
  * Additive fields are a compatible change; changing or removing an existing
118
128
  * field requires bumping this version number (see VERSIONING.md).
129
+ * @public
119
130
  */
120
131
  export interface Evidence<TChecks extends CheckSchema = CheckSchema> {
121
132
  /** Schema version of this shape; see VERSIONING.md. */
@@ -139,6 +150,7 @@ export interface Evidence<TChecks extends CheckSchema = CheckSchema> {
139
150
  * By the time any policy runs, every check has already finished executing
140
151
  * and every check's evidence has already been assembled -- no policy ever
141
152
  * observes a partially-populated `evidence` (see specs/architecture.md).
153
+ * @public
142
154
  */
143
155
  export interface PolicyContext<TChecks extends CheckSchema = CheckSchema> {
144
156
  /** This check's own evidence. */
@@ -167,6 +179,7 @@ export interface PolicyContext<TChecks extends CheckSchema = CheckSchema> {
167
179
  * condition is materially relevant and wants it surfaced -- not a synonym
168
180
  * for "minor failure"; a `warn` never fails `Verdict.passed` (see
169
181
  * `runPolicies` in `src/policy/run-policies.ts`).
182
+ * @public
170
183
  */
171
184
  export type PolicyOutcome = "pass" | "fail" | "warn";
172
185
  /**
@@ -181,12 +194,18 @@ export type PolicyOutcome = "pass" | "fail" | "warn";
181
194
  * consumer to understand *why* the policy reached its outcome from this
182
195
  * value alone. A rationale like "see output above" or "check the report for
183
196
  * details" defeats the purpose: it forces the consumer back to raw,
184
- * unstructured command output, exactly what this type exists to avoid. See
185
- * specs/architecture.md for the evidence/rationale/judgment distinction this
197
+ * unstructured command output, exactly what this type exists to avoid.
198
+ * That specific anti-pattern is not just documented here -- `runPolicies`
199
+ * (`src/policy/run-policies.ts`'s `VAGUE_RATIONALE_PATTERNS`, ADR 0016) rejects a
200
+ * rationale matching it at runtime, the same as any other malformed
201
+ * `PolicyResult`, so a check (repo-contract's own, or a consumer's) that
202
+ * regresses to a vague deferral fails loudly instead of silently shipping.
203
+ * See specs/architecture.md for the evidence/rationale/judgment distinction this
186
204
  * type is built around: evidence answers "what happened?", `rationale`
187
205
  * answers "what does the repository's policy conclude about what
188
206
  * happened?", and a policy's `outcome` is not the final word -- a human or
189
207
  * AI consumer still makes the final judgment call using both.
208
+ * @public
190
209
  */
191
210
  export interface PolicyResult {
192
211
  /** The policy's pass/fail/warn decision. */
@@ -202,9 +221,13 @@ export interface PolicyResult {
202
221
  * synchronous or return a `Promise`. repo-contract does not interpret
203
222
  * `rationale` beyond storing and surfacing it verbatim; the package has no
204
223
  * opinion about what makes a check pass, fail, or warrant a `warn`.
224
+ * @public
205
225
  */
206
226
  export type Policy<TChecks extends CheckSchema = CheckSchema> = (ctx: PolicyContext<TChecks>) => PolicyResult | Promise<PolicyResult>;
207
- /** One partially configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable. */
227
+ /**
228
+ * One partially configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable.
229
+ * @public
230
+ */
208
231
  export interface CheckDefinitionConfig {
209
232
  /**
210
233
  * The command to run. A `string` is tokenized into executable + arguments
@@ -282,7 +305,10 @@ export interface CheckDefinitionConfig {
282
305
  /** Decides whether this check's evidence is acceptable, once its process has finished running. */
283
306
  readonly policy: Policy;
284
307
  }
285
- /** One fully configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable. */
308
+ /**
309
+ * One fully configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable.
310
+ * @public
311
+ */
286
312
  export interface CheckDefinition extends CheckDefinitionConfig {
287
313
  /**
288
314
  * Other check ids (from this same `checks` record) that must reach a
@@ -303,7 +329,10 @@ export interface CheckDefinition extends CheckDefinitionConfig {
303
329
  */
304
330
  readonly dependsOn?: readonly string[];
305
331
  }
306
- /** The full set of checks in a `RepoContractConfig`, keyed by check id. */
332
+ /**
333
+ * The full set of checks in a `RepoContractConfig`, keyed by check id.
334
+ * @public
335
+ */
307
336
  export type CheckSchema = Record<string, CheckDefinition>;
308
337
  /**
309
338
  * Same shape as a check schema `T`, except each check's own `dependsOn` is
@@ -322,6 +351,7 @@ export type CheckSchema = Record<string, CheckDefinition>;
322
351
  * `TChecks` directly from a mapped/conditional type over itself, as this
323
352
  * type is, loses that contextual typing -- confirmed during implementation,
324
353
  * not a hypothetical).
354
+ * @public
325
355
  */
326
356
  export type ValidatedCheckSchema<T> = {
327
357
  readonly [K in keyof T]: T[K] extends CheckDefinitionConfig ? Omit<T[K], "dependsOn"> & {
@@ -338,6 +368,7 @@ export type ValidatedCheckSchema<T> = {
338
368
  * capability: it calls it with a resolved command/argv/options and does not
339
369
  * inspect, wrap, or sanitize it -- the security properties of the spawned
340
370
  * process are entirely the supplied function's own.
371
+ * @public
341
372
  */
342
373
  export type Spawner = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
343
374
  /**
@@ -348,9 +379,13 @@ export type Spawner = (command: string, args: readonly string[], options: SpawnO
348
379
  * a signal-handling context that cannot `await` anything else -- see
349
380
  * specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
350
381
  * `node:child_process.spawnSync` and cross-spawn's exported `sync` are both valid, drop-in values.
382
+ * @public
351
383
  */
352
384
  export type SyncSpawner = (command: string, args: readonly string[], options: SpawnSyncOptions) => SpawnSyncReturns<Buffer | string>;
353
- /** Top-level configuration passed to `defineRepoContract`/`runRepoContract`. */
385
+ /**
386
+ * Top-level configuration passed to `defineRepoContract`/`runRepoContract`.
387
+ * @public
388
+ */
354
389
  export interface RepoContractConfig<TChecks extends CheckSchema = CheckSchema> {
355
390
  /** Every check to run, keyed by check id. */
356
391
  readonly checks: TChecks;
@@ -399,7 +434,10 @@ export interface RepoContractConfig<TChecks extends CheckSchema = CheckSchema> {
399
434
  */
400
435
  readonly killProcessTree?: SyncSpawner;
401
436
  }
402
- /** Optional per-run controls for `runRepoContract`. */
437
+ /**
438
+ * Optional per-run controls for `runRepoContract`.
439
+ * @public
440
+ */
403
441
  export interface RunRepoContractOptions {
404
442
  /** Abort the entire run. Checks already in flight are terminated; checks not yet started never spawn. Every configured check still receives a well-formed evidence entry (`status: "aborted"`) and still has its policy invoked. */
405
443
  readonly signal?: AbortSignal;
@@ -417,6 +455,7 @@ export interface RunRepoContractOptions {
417
455
  * schema-versioning policy) -- `version: 2` reflects `checks[id]` changing
418
456
  * shape from `{ passed, reason? }` to a full `PolicyResult`
419
457
  * (`{ outcome, rationale }`); see ADR 0001.
458
+ * @public
420
459
  */
421
460
  export interface Verdict<TChecks extends CheckSchema = CheckSchema> {
422
461
  /** Schema version of this shape; see VERSIONING.md. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/helpers/exception-policy.ts","../src/helpers/load-exception-registry.ts","../src/helpers/reconcile-exceptions.ts","../src/helpers/write-exception-registry.ts"],"names":["minimatch","createHash","isPlainObject","readFileFromFs","writeFileFromFs","renameFromFs","randomUUID","lstatFromFs"],"mappings":"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","file":"helpers.cjs","sourcesContent":["import { createHash } from \"node:crypto\"\nimport { minimatch } from \"minimatch\"\n\n/**\n * A resolved decision for one `{ group, category }` classification. `\"forbidden\"`: never\n * permitted, regardless of any field's content. `\"allowed\"`: permitted unconditionally -- no\n * field is required. `\"exception\"`: permitted only once every field named in `requirements` is a\n * non-empty (post-`.trim()`) string on the record being evaluated (see `evaluateExceptionRecord`).\n *\n * Deliberately not a numeric \"how many details are required\" threshold -- a plain count is\n * trivially satisfied by generating that many generic-sounding filler entries without doing any of\n * the underlying work the count was meant to prove happened. Naming exactly which fields must be\n * filled in makes each one individually reviewable against a specific question instead. This is\n * the same design `scripts/suppression-governance/policy-config.ts`'s `SuppressionPolicy`\n * establishes for the disable-comment domain this type generalizes.\n */\nexport type ExceptionPolicy =\n | { readonly mode: \"forbidden\" }\n | { readonly mode: \"allowed\" }\n | { readonly mode: \"exception\"; readonly requirements: readonly string[] }\n\n/**\n * One classification group's own policy -- `default` is this group's fallback for any `category`\n * with no exact or glob match in `rules` (see `resolveExceptionPolicy` for the full exact > glob >\n * group-default > global-default precedence). Omit `default` to fall through to the caller-supplied\n * `globalDefault` instead. Each key of `rules` is either an exact `category` string or a\n * `minimatch` glob pattern -- `resolveExceptionPolicy` tries an exact match first and only\n * consults glob matching once no exact key exists, so a glob can never shadow a more specific\n * exact entry.\n */\nexport interface ExceptionCategoryGroup {\n /** This group's fallback policy when `category` matches neither an exact nor a glob key in `rules`. Falls through to the caller's `globalDefault` when omitted. */\n readonly default?: ExceptionPolicy\n /** Keyed by exact `category` string or `minimatch` glob pattern. A literal `\"*\"` key is rejected by `validateExceptionPolicyConfig` -- see that function's own doc comment for why. */\n readonly rules?: Readonly<Record<string, ExceptionPolicy>>\n}\n\n/**\n * A full exception-policy configuration, keyed by classification `group` (e.g. a tool name, or a\n * suppression domain). The core never invents the names \"domain\"/\"rule\"/\"severity\"/\n * \"exceptionType\" -- those are every consumer's own vocabulary, expressed here purely as\n * `{ group, category }` (see `ExceptionClassification`).\n */\nexport type ExceptionPolicyConfig = Readonly<Record<string, ExceptionCategoryGroup>>\n\n/**\n * One classification a record is evaluated against -- deliberately just `{ group, category }`,\n * never `domain`/`rule`/`severity`/`exceptionType` or any other consumer-specific vocabulary. A\n * consumer maps its own domain concepts onto this shape at the call site (e.g.\n * `suppression-governance`'s `{ group: record.domain, category: rule }` per suppressed rule;\n * `security-socket`'s `{ group: \"socket\", category: normalizedSeverity }`) -- the core itself\n * never interprets `group`/`category` beyond using them as lookup keys into an\n * `ExceptionPolicyConfig`.\n */\nexport interface ExceptionClassification {\n /** The top-level key this classification resolves against in an `ExceptionPolicyConfig`. */\n readonly group: string\n /** The `rules` key (exact or glob-matched) this classification resolves against within `group`. */\n readonly category: string\n}\n\n/** A resolved judgment about one record, once its `ExceptionPolicy` has been checked against its own field values. See `ExceptionDeterminant`. */\nexport type ExceptionVerdict = \"forbidden\" | \"insufficient\" | \"permitted\"\n\n/** One record's resolved policy verdict, and (for `\"insufficient\"`) which required fields are still empty. Never published as a batch/matched-vs-unmatched shape -- matching a finding to a record stays entirely check-owned (each check reconciles its own findings against its own registry via `reconcileExceptions`). */\nexport interface ExceptionDeterminant<TRecord> {\n /** The record this determinant was computed for, returned verbatim. */\n readonly record: TRecord\n /** `\"forbidden\"`: never permitted. `\"insufficient\"`: exception-eligible, but `missing` is non-empty. `\"permitted\"`: every required field is filled in (or the resolved policy was `\"allowed\"`). */\n readonly verdict: ExceptionVerdict\n /** Every required field (by name) still empty on `record`, in the resolved policy's own `requirements` order. Always `[]` for `\"forbidden\"`/`\"permitted\"`. */\n readonly missing: readonly string[]\n}\n\n/**\n * The stricter of two policies: `\"forbidden\"` always wins outright; between two non-forbidding\n * policies, the union of their required fields wins (an `\"allowed\"` policy contributes no\n * requirements, so merging it with an `\"exception\"` policy just yields that same exception\n * unchanged) -- satisfying the union trivially satisfies each individual input policy too. Reused\n * for two distinct combinations: multiple glob patterns matching the same category, and multiple\n * classifications resolved for the same record (see `evaluateExceptionRecord`). The union preserves\n * `a`'s own field order first, then appends any of `b`'s fields not already present -- deterministic\n * given deterministic inputs, without needing a fixed, closed field-name enum the way\n * `resolve-policy.ts`'s own `REQUIREMENT_ORDER` does (this core never owns a closed vocabulary of\n * field names; see `checks/shared/exception-record.ts` for where a consumer's own closed\n * `EXCEPTION_TYPES`-style enum lives instead).\n * @param a - One resolved policy.\n * @param b - The other resolved policy.\n * @returns The stricter of `a` and `b`.\n */\nexport function stricterOf(a: ExceptionPolicy, b: ExceptionPolicy): ExceptionPolicy {\n if (a.mode === \"forbidden\" || b.mode === \"forbidden\") return { mode: \"forbidden\" }\n\n const aRequirements = a.mode === \"exception\" ? a.requirements : []\n const bRequirements = b.mode === \"exception\" ? b.requirements : []\n\n if (aRequirements.length === 0 && bRequirements.length === 0) return { mode: \"allowed\" }\n\n const requirements = [...aRequirements]\n for (const requirement of bRequirements) {\n if (!requirements.includes(requirement)) requirements.push(requirement)\n }\n\n return { mode: \"exception\", requirements }\n}\n\n/**\n * Resolves the policy one `{ group, category }` classification is subject to, in strict precedence\n * order -- documented here as this function's one authoritative source of truth for that order\n * (generalized from `scripts/suppression-governance/resolve-policy.ts`'s own `resolveRequirement`,\n * which this function's future retrofit replaces):\n *\n * 0. **No entry for `group`** -- `config[group] === undefined` -- resolves to `globalDefault`\n * immediately, before `category` is even inspected (so a blanket `category === \"*\"` against an\n * unconfigured group still just returns `globalDefault`, never an empty-array reduce).\n * 1. **Blanket category** -- `category === \"*\"` means every category this group could ever apply\n * to was matched at once, not one specific category literally named `\"*\"`. It resolves as the\n * strictest (`stricterOf`) policy across every entry in `group.rules` plus the group's own\n * default (or `globalDefault`) -- never via `minimatch`: `minimatch(\"*\", pattern)` tests the\n * literal one-character string `\"*\"` as a path against `pattern`, which does not glob-match a\n * pattern like `\"security/*\"` (that would require the *pattern*, not the *target*, to be `\"*\"`),\n * so treating this case as an ordinary pattern match would silently let a blanket match fall\n * through a group's `forbidden` rules into its far more lenient default.\n * 2. **Exact match** -- `group.rules[category]`, if present. A glob is never even consulted once an\n * exact entry exists for `category`.\n * 3. **Glob match** -- the strictest (`stricterOf`) policy among every key in `group.rules` that is\n * not itself an exact match for `category` but does match it as a `minimatch` glob (e.g.\n * `\"security/*\"` matching `\"security/detect-object-injection\"`).\n * 4. **Group default** -- `group.default`, if `group` itself has an entry in `config` (whether or\n * not that entry defines its own `default`).\n * 5. **Global default** -- `globalDefault`, used only when `group` itself has no entry in `config`\n * at all, or when neither an exact/glob match nor a `group.default` applies.\n *\n * Assumes `config` has already passed `validateExceptionPolicyConfig`.\n * @param classification - The `{ group, category }` pair to resolve a policy for.\n * @param config - The exception policy configuration to resolve against.\n * @param globalDefault - The policy to fall back to when `classification.group` has no entry in `config` at all.\n * @returns The resolved policy for `classification`.\n */\nexport function resolveExceptionPolicy(\n classification: ExceptionClassification,\n config: ExceptionPolicyConfig,\n globalDefault: ExceptionPolicy,\n): ExceptionPolicy {\n const { group, category } = classification\n const groupPolicy = config[group]\n if (groupPolicy === undefined) return globalDefault\n\n const rules = Object.entries(groupPolicy.rules ?? {})\n\n if (category === \"*\") {\n const everyPolicy = [...rules.map(([, policy]) => policy), groupPolicy.default ?? globalDefault]\n return everyPolicy.reduce(stricterOf)\n }\n\n const exactMatch = rules.find(([pattern]) => pattern === category)\n if (exactMatch) return exactMatch[1]\n\n const globMatches = rules\n .filter(([pattern]) => {\n // Equivalent mutant: by the time this line runs, `exactMatch` above has already returned\n // for any entry whose `pattern` literally equals `category`, so no remaining entry in\n // `rules` can ever have `pattern === category` here -- `pattern !== category` is therefore\n // always `true` at this point, and no test could ever distinguish it from the literal\n // `true` a mutant substitutes for it.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n return pattern !== category && minimatch(category, pattern)\n })\n .map(([, policy]) => policy)\n if (globMatches.length > 0) {\n return globMatches.reduce(stricterOf)\n }\n\n return groupPolicy.default ?? globalDefault\n}\n\n/**\n * One record to evaluate, paired with everything `evaluateExceptionRecord` needs to judge it --\n * shared by `evaluateExceptionRecord` and `evaluateExceptionRecords` (whose own `inputs` is just\n * `readonly ExceptionRecordEvaluation<TRecord>[]`) so the same five-field shape isn't declared\n * twice.\n */\nexport interface ExceptionRecordEvaluation<TRecord> {\n /** The record to evaluate. */\n readonly record: TRecord\n /** Every classification this record is subject to; the strictest resolved policy across all of them wins. */\n readonly classifications: readonly [ExceptionClassification, ...ExceptionClassification[]]\n /** The exception policy configuration to resolve `classifications` against. */\n readonly config: ExceptionPolicyConfig\n /** The policy to fall back to for any classification whose `group` has no entry in `config` at all. */\n readonly globalDefault: ExceptionPolicy\n /** Resolves one named required field's current string value on `record`. */\n readonly fieldValue: (record: TRecord, requirement: string) => string\n}\n\n/**\n * Evaluates one record against every one of its own classifications, taking the strictest\n * (`stricterOf`) of each classification's resolved policy -- a record matching both a forbidden\n * classification and an otherwise-fine one is forbidden overall. `classifications` is a non-empty\n * tuple by type: zero classifications would be a caller bug (which classification would silently\n * fall back to `globalDefault`?), never a case this function has to guess about at runtime. A\n * required field only counts as satisfied once `fieldValue(record, requirement).trim()` is\n * non-empty -- an empty field is valid *data*, but policy-insufficient, exactly as `\"exception\"`\n * mode's name implies. `fieldValue` may resolve a dotted path (e.g. `\"verification.verifiedBy\"`)\n * or anything else a consumer's own record shape needs -- this function never interprets\n * `requirement` itself, it only ever calls `fieldValue(record, requirement)` and trims the result.\n * @param input - The record to evaluate, its classifications, the policy configuration and global default to resolve them against, and the field-value accessor -- see `ExceptionRecordEvaluation`'s own per-field doc comments.\n * @returns The record's verdict, and which required fields (if any) are still missing.\n */\nexport function evaluateExceptionRecord<TRecord>(\n input: ExceptionRecordEvaluation<TRecord>,\n): ExceptionDeterminant<TRecord> {\n const { record, classifications, config, globalDefault, fieldValue } = input\n\n const resolvedPolicy = classifications\n .map((classification) => resolveExceptionPolicy(classification, config, globalDefault))\n .reduce(stricterOf)\n\n if (resolvedPolicy.mode === \"forbidden\") {\n return { record, verdict: \"forbidden\", missing: [] }\n }\n\n const requirements = resolvedPolicy.mode === \"exception\" ? resolvedPolicy.requirements : []\n const missing = requirements.filter(\n (requirement) => fieldValue(record, requirement).trim().length === 0,\n )\n\n return { record, verdict: missing.length === 0 ? \"permitted\" : \"insufficient\", missing }\n}\n\n/**\n * A thin batch over already-matched `(record, classifications)` pairs -- exactly\n * `inputs.map(evaluateExceptionRecord)`, provided so a caller evaluating many records against the\n * same `config`/`globalDefault`/`fieldValue` doesn't have to write that `.map` itself. Matching a\n * record to a finding in the first place stays entirely check-owned (each check reconciles its own\n * findings against its own registry) -- this function takes already-paired inputs, it never does\n * any matching of its own.\n * @param inputs - Each already-matched record to evaluate, in the same shape `evaluateExceptionRecord` itself takes.\n * @returns Each input's own determinant, in the same order as `inputs`.\n */\nexport function evaluateExceptionRecords<TRecord>(\n inputs: readonly ExceptionRecordEvaluation<TRecord>[],\n): readonly ExceptionDeterminant<TRecord>[] {\n return inputs.map((input) => evaluateExceptionRecord(input))\n}\n\nconst VALID_MODES = [\"forbidden\", \"allowed\", \"exception\"] as const\n\n/**\n * Validates one `ExceptionPolicy` value's shape -- `mode` must be one of the three recognized\n * modes, and an `\"exception\"` mode's `requirements` must be a non-empty array (an empty array is\n * rejected rather than silently treated as equivalent to `\"allowed\"` -- if nothing is required,\n * `\"allowed\"` is the correct, unambiguous way to say so). When `validRequirements` is supplied,\n * every named requirement must also be a member of it.\n * @param value - The candidate policy value to validate.\n * @param location - Where this value lives in the configuration, for error messages.\n * @param validRequirements - The complete set of field names this consumer's policy may require, if the consumer wants that checked. Omit to skip this check entirely (any string is accepted as a requirement name).\n * @param errors - Accumulates every configuration problem found.\n */\nfunction validateExceptionPolicyValue(\n value: unknown,\n location: string,\n validRequirements: readonly string[] | undefined,\n errors: string[],\n): void {\n if (typeof value !== \"object\" || value === null) {\n errors.push(`${location} must be an object.`)\n return\n }\n\n const { mode, requirements } = value as Record<string, unknown>\n\n if (mode === \"forbidden\" || mode === \"allowed\") return\n\n if (mode !== \"exception\") {\n errors.push(\n `${location}.mode must be one of ${VALID_MODES.map((m) => `\"${m}\"`).join(\", \")} (got ${JSON.stringify(mode)}).`,\n )\n return\n }\n\n if (!Array.isArray(requirements) || requirements.length === 0) {\n errors.push(`${location}.requirements must be a non-empty array when mode is \"exception\".`)\n return\n }\n\n // Every requirement name must be a string regardless of whether `validRequirements` was\n // supplied -- `evaluateExceptionRecord`'s `fieldValue(record, requirement)` expects a string\n // key, and a decoded-but-unvalidated policy (e.g. straight from untrusted JSON) could otherwise\n // carry a number/object/null entry through to that call. `validRequirements`, when supplied,\n // additionally narrows to a specific allowed set; when omitted, \"is a string\" is still checked.\n for (const requirement of requirements) {\n if (typeof requirement !== \"string\") {\n errors.push(\n `${location}.requirements contains a non-string entry (got ${JSON.stringify(requirement)}).`,\n )\n continue\n }\n if (validRequirements !== undefined && !validRequirements.includes(requirement)) {\n errors.push(\n `${location}.requirements contains an invalid entry (got ${JSON.stringify(requirement)}); ` +\n `expected one of ${validRequirements.map((r) => `\"${r}\"`).join(\", \")}.`,\n )\n }\n }\n}\n\n/**\n * Whether `value` is a non-null, non-array object -- the shape every group's own entry in a\n * `ExceptionPolicyConfig`, and every group's `rules` container, is expected to be before its own\n * fields are inspected. `config`'s declared type (`ExceptionPolicyConfig`) does not, by itself,\n * guarantee this at runtime: this function exists to validate a config that may have been decoded\n * from untrusted JSON and only cast to that type, not actually shaped like it.\n * @param value - The candidate value to check.\n * @returns `true` if `value` is a plain object.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/**\n * Validates an entire `ExceptionPolicyConfig`'s shape -- every group's `default` and every entry\n * in its `rules` must be a well-formed `ExceptionPolicy` (see `validateExceptionPolicyValue`), and\n * no group's `rules` may use the literal `\"*\"` as a key. A literal `\"*\"` key is always a mistake,\n * never an intentional blanket policy: `resolveExceptionPolicy`'s own blanket-category handling is\n * triggered by the *input* `category` being `\"*\"`, not by a `\"*\"` entry in `rules` -- a `\"*\"` rules\n * key would instead be consulted only as an ordinary `minimatch` glob, which matches the literal\n * one-character string `\"*\"` as a *target*, not as a wildcard pattern matching every real category\n * name (`minimatch(\"*\", pattern)` truthiness depends on `pattern`, not the other way around) --\n * see `resolveExceptionPolicy`'s own doc comment, case 1, for the failure mode this prevents.\n * @param config - The exception policy configuration to validate.\n * @param validRequirements - The complete set of field names this consumer's policy may require, if the consumer wants that checked. Omit to skip that check entirely.\n * @returns Every configuration problem found; empty if `config` is valid.\n */\nexport function validateExceptionPolicyConfig(\n config: ExceptionPolicyConfig,\n validRequirements?: readonly string[],\n): readonly string[] {\n const errors: string[] = []\n\n for (const [group, groupPolicy] of Object.entries(config)) {\n // `groupPolicy`'s declared type (`ExceptionCategoryGroup`) does not guarantee this shape at\n // runtime -- a config decoded from untrusted JSON and merely cast to `ExceptionPolicyConfig`\n // could carry `null`, a string, or an array here. Reject it before dereferencing `.default`/\n // `.rules`, rather than throwing (`null.default`) or silently treating a non-object as an\n // empty group (both real failure modes this guard closes).\n if (!isPlainObject(groupPolicy)) {\n errors.push(`config.${group} must be an object.`)\n continue\n }\n\n if (groupPolicy.default !== undefined) {\n validateExceptionPolicyValue(\n groupPolicy.default,\n `config.${group}.default`,\n validRequirements,\n errors,\n )\n }\n\n if (groupPolicy.rules !== undefined && !isPlainObject(groupPolicy.rules)) {\n errors.push(`config.${group}.rules must be an object.`)\n continue\n }\n\n // No cast needed here: the guard above has already narrowed `groupPolicy.rules` to\n // `Record<string, unknown> | undefined` via `isPlainObject`'s type predicate.\n for (const [pattern, policy] of Object.entries(groupPolicy.rules ?? {})) {\n if (pattern === \"*\") {\n errors.push(\n `config.${group}.rules must not use the literal \"*\" as a key -- it would be consulted ` +\n 'only as an ordinary minimatch glob (matching the literal one-character category \"*\", ' +\n \"never every category in the group) rather than as the blanket policy \" +\n \"`resolveExceptionPolicy` already applies whenever the classification's own `category` \" +\n 'is \"*\". Omit this key, or use a more specific pattern.',\n )\n }\n validateExceptionPolicyValue(\n policy,\n `config.${group}.rules[\"${pattern}\"]`,\n validRequirements,\n errors,\n )\n }\n }\n\n return errors\n}\n\n/**\n * A deterministic digest of a set of fields' current values on `record`, via `node:crypto` --\n * pure, synchronous, no ambient state (Socket.dev has no alert on `node:crypto`; it is not\n * `child_process`/`process.env`, the two capabilities `src/helpers/**` must never touch -- see\n * `scripts/verify-no-ambient-capabilities.mjs`). This is what makes a \"verification\" *content-bound*\n * rather than merely attested: a consumer records this hash (see `ExceptionVerification` in\n * `checks/shared/exception-record.ts`) at the moment a human or a mechanical re-check approved a\n * record's current prose; recomputing it later and comparing is how staleness is detected with no\n * separate tracking logic -- edit any field named in `fields` and the hash silently stops matching.\n * Each field's name is bound into the digest alongside its value, both serialized via\n * `JSON.stringify` as a `[name, value]` pair -- JSON's own escaping makes the digest unambiguous\n * regardless of what characters a field's name or value contains, so two different field sets\n * whose values happen to concatenate identically as plain text can never collide here.\n * @param record - The record to hash fields from.\n * @param fields - Which fields (by name, in this exact order) to include in the digest -- the same names `fieldValue` would be called with by `evaluateExceptionRecord`.\n * @param fieldValue - Resolves one named field's current string value on `record`, exactly like `evaluateExceptionRecord`'s own `fieldValue` parameter.\n * @returns A hex-encoded SHA-256 digest of `fields`' current values.\n */\nexport function hashRequirementFields<TRecord>(\n record: TRecord,\n fields: readonly string[],\n fieldValue: (record: TRecord, requirement: string) => string,\n): string {\n const canonical = JSON.stringify(fields.map((field) => [field, fieldValue(record, field)]))\n // Equivalent mutant: Node's Hash.update(data, inputEncoding) treats a falsy/empty\n // inputEncoding identically to \"utf8\" for a string `data` argument -- confirmed directly:\n // createHash(\"sha256\").update(x, \"utf8\").digest(\"hex\") === createHash(\"sha256\").update(x,\n // \"\").digest(\"hex\") for every input tried. No test could ever distinguish \"utf8\" from \"\" at\n // this exact call site.\n // Stryker disable next-line StringLiteral -- equivalent mutant, see comment above.\n return createHash(\"sha256\").update(canonical, \"utf8\").digest(\"hex\")\n}\n","import { readFile as readFileFromFs } from \"node:fs/promises\"\nimport type { StandardSchemaV1 } from \"../standard-schema/types.js\"\n\n/**\n * Renders one failed-validation issue as `path: message`, or just `message` when it has no path --\n * the same shape `src/parsing/format-schema-issues.ts` produces for `output.schema` failures, kept\n * as an independent, much smaller copy here rather than an import: `src/helpers/**` is a second,\n * independent published barrel (see `src/presets/index.ts`'s own doc comment on the same point) and\n * deliberately never reaches into `src/parsing/`, an internal layer of the root barrel it has no\n * business depending on.\n * @param issue - one issue from a failed `StandardSchemaV1.Result`.\n * @returns the rendered issue.\n */\nfunction formatIssue(issue: StandardSchemaV1.Issue): string {\n if (issue.path === undefined || issue.path.length === 0) return issue.message\n\n const path = issue.path\n // eslint-disable-next-line secure-coding/no-improper-type-validation -- `segment` is declared `PropertyKey | StandardSchemaV1.PathSegment` (never `null` or an array), so `typeof segment === \"object\"` can only match the `PathSegment` object form here; the null/array-safe rewrite this rule suggests is flagged as an unreachable condition by @typescript-eslint/no-unnecessary-condition given that exact type, so satisfying both rules at once is impossible -- the type itself is the guarantee this check would otherwise add at runtime.\n .map((segment) => (typeof segment === \"object\" ? segment.key : segment))\n .map((key, index) => {\n if (typeof key === \"number\") return `[${String(key)}]`\n const rendered = String(key)\n return index === 0 ? rendered : `.${rendered}`\n })\n .join(\"\")\n\n return `${path}: ${issue.message}`\n}\n\n/**\n * Whether `value` is a non-`null`, non-array object -- the shape an exception registry's on-disk\n * envelope must be before its own `exceptions` field is inspected.\n * @param value - the candidate value to check.\n * @returns `true` if `value` is a plain object.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reads and validates one exception registry file from disk -- `path -> { \"$schema\"?: string,\n * \"exceptions\": T[] }`, in two independently-validated layers. This function owns only the\n * envelope: that the file exists (or is absent, a normal empty-registry state), parses as JSON,\n * and is an object carrying an `\"exceptions\"` field at all. It never inspects what is inside\n * `exceptions` beyond that -- `schema` (a `StandardSchemaV1`, hand-written or from a real library;\n * see `src/standard-schema/types.ts`) owns every field-level concern for the caller's own record\n * shape, exactly the same \"consumer supplies a trusted capability, this package calls it without\n * owning its internals\" relationship `RepoContractConfig.spawn`/`env` already establish (see\n * `specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md`).\n *\n * `readFile` defaults to `node:fs/promises`' own `readFile` -- already used by\n * `src/presets/security-secrets.ts`, so this introduces no new filesystem-access surface; a\n * missing file is a normal \"no exceptions recorded yet\" state, not an error (`{ ok: true, records:\n * [] }`), matching `scripts/suppression-governance/check.ts`'s own `loadExistingRegistry`\n * precedent for the same first-run case. Every other failure -- unreadable-for-another-reason,\n * malformed JSON, a missing/malformed envelope, or a schema validation failure -- returns `{ ok:\n * false, errors }` rather than throwing: a bad registry is reported as data for a check's policy to\n * fail on, never an uncaught exception that crashes the run. The one exception is `schema`'s own\n * `validate()` throwing or rejecting -- a bug in the *caller-supplied schema*, not malformed\n * registry data -- which is deliberately left to propagate as a rejected `Promise`, mirroring\n * `src/parsing/parse-output.ts`'s identical treatment of a throwing `output.schema`.\n * @param input - Where to read the registry from, the schema that validates its `exceptions` array, and (for tests, or a non-`node:fs` environment) an override for how to read `path`.\n * @param input.path - The registry file's path, passed to `input.readFile` verbatim.\n * @param input.schema - Validates (and may transform) the envelope's `exceptions` array once this function's own envelope checks pass.\n * @param input.readFile - Reads `input.path`'s content. Defaults to `node:fs/promises`' own `readFile(path, \"utf8\")`.\n * @returns Every valid record (`ok: true`), or every problem found reading/parsing/validating the file (`ok: false`).\n */\nexport async function loadExceptionRegistry<T>(input: {\n readonly path: string\n readonly schema: StandardSchemaV1<unknown, readonly T[]>\n readonly readFile?: (path: string) => Promise<string>\n}): Promise<{ ok: true; records: readonly T[] } | { ok: false; errors: readonly string[] }> {\n // Equivalent mutant, confirmed empirically: `node:fs/promises`'s `readFile(path, \"\")` returns\n // a raw `Buffer` rather than a decoded string (unlike `Hash.update`, an empty inputEncoding is\n // not treated the same as \"utf8\" here) -- but this function's only use of `raw` is\n // `JSON.parse(raw)` immediately below, and `JSON.parse`'s own `ToString` coercion on a `Buffer`\n // calls `Buffer.prototype.toString()` with no arguments, whose own default encoding is \"utf8\"\n // -- confirmed directly, including with multi-byte UTF-8 content, that\n // `JSON.parse(bufferReadWithEmptyEncoding)` produces byte-identical results to\n // `JSON.parse(stringReadWithUtf8Encoding)` every time. No test could ever observe a difference\n // through this function's own return value.\n // Stryker disable next-line StringLiteral -- equivalent mutant, see comment above.\n const { path, schema, readFile = (target: string) => readFileFromFs(target, \"utf8\") } = input\n\n let raw: string\n try {\n raw = await readFile(path)\n } catch (error) {\n // `readFile` is a caller-supplied capability (see this function's own doc comment) -- a test\n // override, or an unusual real filesystem implementation, could reject with something other\n // than a real `Error` (`null`, a plain string, ...); reading `.code`/`.message` off that\n // directly would throw out of this catch block instead of returning the clean `{ ok: false }`\n // this function promises for every other failure. `isPlainObject` narrows first.\n // Equivalent mutant: `code` is consumed only by the `code === \"ENOENT\"` comparison two lines\n // down, which requires an exact primitive-string match -- there is no value for which\n // `typeof error.code === \"string\"` is false yet `error.code === \"ENOENT\"` is true (a value\n // that literally equals the primitive string \"ENOENT\" always has `typeof` \"string\"). Mutating\n // this `typeof` check to `true` therefore changes what gets assigned to `code` for a\n // non-string `.code` (e.g. a number, or `undefined`), but never changes whether the\n // subsequent `=== \"ENOENT\"` comparison can succeed -- confirmed directly: every plain-object\n // test case covering this line (a numeric code, an absent code) produces the identical\n // not-ENOENT branch and final message either way.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n const code = isPlainObject(error) && typeof error.code === \"string\" ? error.code : undefined\n if (code === \"ENOENT\") return { ok: true, records: [] }\n const message =\n isPlainObject(error) && typeof error.message === \"string\" ? error.message : String(error)\n return { ok: false, errors: [`Could not read ${path}: ${message}`] }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (error) {\n return { ok: false, errors: [`${path} is not valid JSON: ${(error as Error).message}`] }\n }\n\n if (!isPlainObject(parsed)) {\n return {\n ok: false,\n errors: [`${path} must contain a JSON object with an \"exceptions\" array (got a non-object).`],\n }\n }\n\n const { exceptions } = parsed\n if (exceptions === undefined) {\n return { ok: false, errors: [`${path} is missing its required \"exceptions\" field.`] }\n }\n\n const result = await schema[\"~standard\"].validate(exceptions)\n if (result.issues !== undefined) {\n return { ok: false, errors: result.issues.map((issue) => formatIssue(issue)) }\n }\n\n return { ok: true, records: result.value }\n}\n","/**\n * The registry-lifecycle half of `repo-contract/helpers` -- `reconcileExceptions` diffs a check's\n * raw findings against its already-loaded exception registry, and `serializeExceptionRegistry`\n * renders the reconciled records back to their canonical on-disk form. Both are pure; the only\n * I/O is `writeExceptionRegistry` in its own module. See\n * specs/decisions/0013-reusable-exception-policy-helper.md's \"The exception registry is the review\n * surface\" section for the model these serve: a check emits 100% of its findings, reconciliation\n * maintains one record per finding (a fresh stub for a new one), and stale records are surfaced,\n * never removed.\n */\n\n/**\n * The three fields every exception record in `.repo-contract/exceptions/*.json` carries, whatever\n * the owning check. A registry adds its own typed fields on top; this is the shared core the\n * generic machinery (`reconcileExceptions`, `serializeExceptionRegistry`,\n * `validateExceptionRegistry` in the check-owned layer) relies on.\n */\nexport interface ExceptionRecordCore {\n /** The check-namespaced semantic identity of the finding this record waives (e.g. `\"suppression:eslint:no-console:src/foo.ts:<module>\"`). Equals the finding id; a record whose id matches no current finding is stale. Never derived from prose. */\n readonly id: string\n /** Record-schema version. */\n readonly version: number\n /** The one human-authored field: why this guardrail is deliberately bypassed, and what was checked to confirm the finding is real. `\"\"` in a freshly scaffolded stub (the policy, not the validator, rejects a blank/placeholder value). */\n readonly justification: string\n}\n\n/** The outcome of reconciling one run's findings against one exception registry. */\nexport interface ExceptionReconciliation<TFinding, TRecord> {\n /** Each finding paired with the existing record it matched, in `findings` order. */\n readonly matchedPairs: readonly { readonly finding: TFinding; readonly record: TRecord }[]\n /** Every record to persist as live: matched records verbatim, plus one fresh `createStub` per unmatched finding. Never contains a stale record. */\n readonly activeRecords: readonly TRecord[]\n /** Existing records whose id matched no finding this run -- surfaced for the policy to fail on, **never removed** by this function (retiring one is an explicit human edit). */\n readonly staleRecords: readonly TRecord[]\n /** The ids of the stubs created this run -- a subset of `activeRecords`' ids. */\n readonly newStubIds: readonly string[]\n}\n\n/**\n * Reconciles this run's raw findings against the check's already-loaded, already-validated\n * exception registry. Pure and add-only: it never mutates a matched record and never removes a\n * stale one.\n *\n * `deriveId` must be **injective** over `findings` -- every independently-governable finding needs\n * a distinct id, or the mechanism cannot tell which finding a record's `justification` belongs to.\n * A collision is returned as `{ ok: false }` (a real condition a check must surface: the source\n * genuinely holds two identical directives), naming both finding indices so the check can point a\n * developer at them.\n *\n * `createStub(finding, id)` is handed the canonical id and its result's `id` is asserted equal to\n * it -- a `createStub` that derives its own identity differently is a `{ ok: false }` bug report,\n * never a silently-mismatched record.\n *\n * Precondition: `existing` has already passed the check's registry validator (unique ids, correct\n * namespace). This function does not re-validate it.\n * @param input - The already-loaded registry, this run's findings, and the check-owned identity + stub callbacks.\n * @param input.existing - The validated records currently on disk.\n * @param input.findings - Every raw finding this run produced -- nothing filtered.\n * @param input.deriveId - The finding's check-namespaced semantic id. Must be injective over `findings`.\n * @param input.createStub - Builds a fresh record for an unmatched finding; receives the canonical id and must return a record carrying it.\n * @returns The reconciliation, or the first integrity problem found.\n */\nexport function reconcileExceptions<TFinding, TRecord extends { readonly id: string }>(input: {\n readonly existing: readonly TRecord[]\n readonly findings: readonly TFinding[]\n readonly deriveId: (finding: TFinding) => string\n readonly createStub: (finding: TFinding, id: string) => TRecord\n}):\n | { readonly ok: true; readonly reconciliation: ExceptionReconciliation<TFinding, TRecord> }\n | { readonly ok: false; readonly error: string } {\n const { existing, findings, deriveId, createStub } = input\n\n const idByFirstIndex = new Map<string, number>()\n const identified: { readonly finding: TFinding; readonly id: string }[] = []\n for (const [index, finding] of findings.entries()) {\n const id = deriveId(finding)\n const prior = idByFirstIndex.get(id)\n if (prior !== undefined) {\n return {\n ok: false,\n error: `deriveId is not injective: findings at index ${String(prior)} and ${String(index)} both map to id ${JSON.stringify(id)} -- every independently-governable finding must have a distinct id; make the two directives distinguishable.`,\n }\n }\n idByFirstIndex.set(id, index)\n identified.push({ finding, id })\n }\n\n const existingById = new Map<string, TRecord>()\n for (const record of existing) existingById.set(record.id, record)\n\n const matchedPairs: { readonly finding: TFinding; readonly record: TRecord }[] = []\n const activeRecords: TRecord[] = []\n const newStubIds: string[] = []\n\n for (const { finding, id } of identified) {\n const match = existingById.get(id)\n if (match !== undefined) {\n matchedPairs.push({ finding, record: match })\n activeRecords.push(match)\n continue\n }\n const stub = createStub(finding, id)\n if (stub.id !== id) {\n return {\n ok: false,\n error: `createStub returned a record whose id ${JSON.stringify(stub.id)} does not equal the canonical id ${JSON.stringify(id)} it was given.`,\n }\n }\n activeRecords.push(stub)\n newStubIds.push(id)\n }\n\n const liveIds = new Set(idByFirstIndex.keys())\n const staleRecords = existing.filter((record) => !liveIds.has(record.id))\n\n return {\n ok: true,\n reconciliation: { matchedPairs, activeRecords, staleRecords, newStubIds },\n }\n}\n\n/**\n * Rebuilds one record with a canonical key order: `id`, `version`, `justification`, then every\n * other own key in code-unit order. Records are flat (scalars and scalar arrays); if a registry\n * ever adds a nested metadata object, its inner keys keep their insertion order until this\n * function is extended. Keys are copied with `Object.defineProperty` so an own `\"__proto__\"` key\n * is carried through as a plain data property rather than silently dropped by the setter (and the\n * prototype is never mutated).\n * @param record - The record to reorder.\n * @returns A new object with the same entries in canonical key order.\n */\nfunction canonicalRecord(\n record: Record<string, unknown> & { readonly id: string },\n): Record<string, unknown> {\n const { id, version, justification, ...rest } = record\n const ordered: Record<string, unknown> = { id, version, justification }\n for (const key of Object.keys(rest).sort()) {\n // `Object.defineProperty`, not `ordered[key] = ...`, so an own `\"__proto__\"` key is stored as\n // a plain enumerable data property instead of being swallowed by the prototype setter. The\n // resulting object is only serialized and discarded, so leaving `writable`/`configurable` at\n // their `false` defaults is fine.\n Object.defineProperty(ordered, key, { value: rest[key], enumerable: true })\n }\n return ordered\n}\n\n/**\n * The canonical on-disk form of one exception registry: `{ \"exceptions\": [ ... ] }`, records\n * sorted ascending by `id` (code-unit order, locale-independent), each record's keys in\n * `canonicalRecord` order, 2-space indent, `\\n` line endings, a trailing newline. Fully\n * deterministic: the same records always serialize byte-for-byte identically, so a reconcile that\n * changes nothing produces no diff and `writeExceptionRegistry` writes nothing.\n *\n * Assumes `records` have unique ids (the caller reconciled them and validated its registry).\n * @param records - The reconciled records (`activeRecords` plus any `staleRecords`, in any order).\n * @returns The exact file contents to persist.\n */\nexport function serializeExceptionRegistry(\n records: readonly (Record<string, unknown> & { readonly id: string })[],\n): string {\n // `records` have unique ids by precondition, so the `<`/`<=` and `>`/`>=` mutants of this\n // comparator, and its `: 0` (tie) branch, are all equivalent -- no two ids ever compare equal.\n // The reversing mutants (swapped `-1`/`1`, the `true`/`false` conditional replacements) are\n // killed by the \"byte-identical regardless of record order\" property test, which serializes a\n // list and its reverse and requires identical bytes. Comparing the `id` strings directly (not\n // the records) also keeps this safe for null-prototype record objects.\n // Stryker disable next-line ConditionalExpression,EqualityOperator -- equivalent mutants, see comment above.\n const sorted = [...records].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n const canonical = sorted.map((record) => canonicalRecord(record))\n return `${JSON.stringify({ exceptions: canonical }, null, 2)}\\n`\n}\n","import {\n lstat as lstatFromFs,\n readFile as readFileFromFs,\n rename as renameFromFs,\n writeFile as writeFileFromFs,\n} from \"node:fs/promises\"\nimport { randomUUID } from \"node:crypto\"\nimport { serializeExceptionRegistry } from \"./reconcile-exceptions.js\"\n\n/**\n * Whether `error` carries `code === \"ENOENT\"` -- a missing file, a normal state the symlink probe\n * and the \"already up to date\" comparison both treat as \"no file yet\", never a failure. Reads\n * `.code` defensively: every filesystem capability here is caller-overridable and could reject\n * with something other than a real `Error`.\n * @param error - the caught value.\n * @returns `true` for an `ENOENT`.\n */\nfunction isEnoent(error: unknown): boolean {\n return (\n // Replacing this `typeof` guard with `true` still yields `false` for every non-object value:\n // a primitive's `.code` is `undefined` (never the string \"ENOENT\"), and `null` is caught by\n // the `!== null` clause. Same accepted equivalent mutant as load-exception-registry.ts's own\n // ENOENT check.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n typeof error === \"object\" &&\n error !== null &&\n (error as { readonly code?: unknown }).code === \"ENOENT\"\n )\n}\n\n/**\n * Writes one exception registry to disk in its canonical form (`serializeExceptionRegistry`),\n * atomically and idempotently -- the sole filesystem-writing primitive in `repo-contract/helpers`.\n *\n * - **Symlinked target** -> `{ ok: false }`: a governance registry is never a symlink, and writing\n * through one would escape `.repo-contract/exceptions/`.\n * - **No file yet** -> written.\n * - **On-disk bytes already equal the canonical form** -> `{ ok: true, written: false }`, no\n * write. A reconcile that changed nothing therefore never dirties the working tree.\n * - **Different** -> the canonical bytes are written to a sibling temp file (`<path>.<uuid>.tmp`)\n * and `rename`d over the target -- the target is replaced atomically, and partial serialized\n * content is never written to the target path. A `rename` failure (e.g. a Windows lock on the\n * target) returns `{ ok: false }` deterministically; the temp file is left in place (harmless, a\n * uuid name, and the run has already failed) -- `.gitignore` covers `.repo-contract/exceptions/*.tmp`.\n *\n * Path containment is the caller's responsibility: repo-contract's own checks always pass\n * `.repo-contract/exceptions/<name>.json`. `readFile`/`writeFile`/`rename`/`isSymlink` default to\n * `node:fs/promises` and are overridable for tests or a non-`node:fs` environment, mirroring\n * `loadExceptionRegistry`'s own `readFile` parameter.\n * @param input - The target path, the records to persist, and (optional) filesystem-capability overrides.\n * @param input.path - Where to write. Passed verbatim to every capability.\n * @param input.records - The reconciled records; serialized via `serializeExceptionRegistry`.\n * @param input.readFile - Reads the current file. Default: `node:fs/promises` `readFile(path, \"utf8\")`.\n * @param input.writeFile - Writes the temp file. Default: `node:fs/promises` `writeFile(path, data, \"utf8\")`.\n * @param input.rename - Replaces the target with the temp file. Default: `node:fs/promises` `rename`.\n * @param input.isSymlink - Whether `path` is a symlink. Default: `lstat(path).isSymbolicLink()`, treating a missing path as not a symlink.\n * @returns `{ ok: true, written }` on success, or `{ ok: false, error }` for a symlink, an unreadable target, or a failed write/rename.\n */\nexport async function writeExceptionRegistry(input: {\n readonly path: string\n readonly records: readonly (Record<string, unknown> & { readonly id: string })[]\n readonly readFile?: (path: string) => Promise<string>\n readonly writeFile?: (path: string, data: string) => Promise<void>\n readonly rename?: (from: string, to: string) => Promise<void>\n readonly isSymlink?: (path: string) => Promise<boolean>\n}): Promise<\n { readonly ok: true; readonly written: boolean } | { readonly ok: false; readonly error: string }\n> {\n const {\n path,\n records,\n // No explicit encoding on either call: `readFile` returns a Buffer that `.toString()` decodes\n // as utf8 by default, and `writeFile` writes a string as utf8 by default -- so there is no\n // encoding literal for a mutant to flip.\n readFile = (target: string) => readFileFromFs(target).then((buffer) => buffer.toString()),\n writeFile = (target: string, data: string) => writeFileFromFs(target, data),\n rename = (from: string, to: string) => renameFromFs(from, to),\n isSymlink = defaultIsSymlink,\n } = input\n\n if (await isSymlink(path)) {\n return {\n ok: false,\n error: `${path} is a symlink; an exception registry must be a regular file inside .repo-contract/exceptions/.`,\n }\n }\n\n const serialized = serializeExceptionRegistry(records)\n\n let current: string | undefined\n try {\n current = await readFile(path)\n } catch (error) {\n if (!isEnoent(error)) {\n return { ok: false, error: `Could not read ${path}: ${String(error)}` }\n }\n }\n if (current === serialized) return { ok: true, written: false }\n\n const tempPath = `${path}.${randomUUID()}.tmp`\n try {\n await writeFile(tempPath, serialized)\n } catch (error) {\n return { ok: false, error: `Could not write ${tempPath}: ${String(error)}` }\n }\n\n try {\n await rename(tempPath, path)\n } catch (error) {\n return {\n ok: false,\n error: `Could not replace ${path} with the reconciled registry (temp file left at ${tempPath}): ${String(error)}`,\n }\n }\n\n return { ok: true, written: true }\n}\n\n/**\n * The default `isSymlink`: `lstat(path).isSymbolicLink()`, with a missing path reported as not a\n * symlink. A non-`ENOENT` `lstat` failure propagates -- a genuinely unreadable path is not\n * silently treated as safe.\n * @param target - the path to probe.\n * @returns whether `target` is a symbolic link.\n */\nasync function defaultIsSymlink(target: string): Promise<boolean> {\n try {\n return (await lstatFromFs(target)).isSymbolicLink()\n } catch (error) {\n // A missing path is \"not a symlink\"; any other `lstat` failure (a non-directory path\n // component -> ENOTDIR, a permissions fault -> EACCES) is rethrown, never silently treated as\n // a safe non-symlink.\n if (isEnoent(error)) return false\n throw error\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/helpers/exception-policy.ts","../src/helpers/load-exception-registry.ts","../src/helpers/reconcile-exceptions.ts","../src/helpers/write-exception-registry.ts"],"names":["minimatch","createHash","isPlainObject","readFileFromFs","writeFileFromFs","renameFromFs","randomUUID","lstatFromFs"],"mappings":"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","file":"helpers.cjs","sourcesContent":["import { createHash } from \"node:crypto\"\nimport { minimatch } from \"minimatch\"\n\n/**\n * A resolved decision for one `{ group, category }` classification. `\"forbidden\"`: never\n * permitted, regardless of any field's content. `\"allowed\"`: permitted unconditionally -- no\n * field is required. `\"exception\"`: permitted only once every field named in `requirements` is a\n * non-empty (post-`.trim()`) string on the record being evaluated (see `evaluateExceptionRecord`).\n *\n * Deliberately not a numeric \"how many details are required\" threshold -- a plain count is\n * trivially satisfied by generating that many generic-sounding filler entries without doing any of\n * the underlying work the count was meant to prove happened. Naming exactly which fields must be\n * filled in makes each one individually reviewable against a specific question instead. This is\n * the same design `scripts/suppression-governance/policy-config.ts`'s `SuppressionPolicy`\n * establishes for the disable-comment domain this type generalizes.\n * @beta\n */\nexport type ExceptionPolicy =\n | { readonly mode: \"forbidden\" }\n | { readonly mode: \"allowed\" }\n | { readonly mode: \"exception\"; readonly requirements: readonly string[] }\n\n/**\n * One classification group's own policy -- `default` is this group's fallback for any `category`\n * with no exact or glob match in `rules` (see `resolveExceptionPolicy` for the full exact > glob >\n * group-default > global-default precedence). Omit `default` to fall through to the caller-supplied\n * `globalDefault` instead. Each key of `rules` is either an exact `category` string or a\n * `minimatch` glob pattern -- `resolveExceptionPolicy` tries an exact match first and only\n * consults glob matching once no exact key exists, so a glob can never shadow a more specific\n * exact entry.\n * @beta\n */\nexport interface ExceptionCategoryGroup {\n /** This group's fallback policy when `category` matches neither an exact nor a glob key in `rules`. Falls through to the caller's `globalDefault` when omitted. */\n readonly default?: ExceptionPolicy\n /** Keyed by exact `category` string or `minimatch` glob pattern. A literal `\"*\"` key is rejected by `validateExceptionPolicyConfig` -- see that function's own doc comment for why. */\n readonly rules?: Readonly<Record<string, ExceptionPolicy>>\n}\n\n/**\n * A full exception-policy configuration, keyed by classification `group` (e.g. a tool name, or a\n * suppression domain). The core never invents the names \"domain\"/\"rule\"/\"severity\"/\n * \"exceptionType\" -- those are every consumer's own vocabulary, expressed here purely as\n * `{ group, category }` (see `ExceptionClassification`).\n * @beta\n */\nexport type ExceptionPolicyConfig = Readonly<Record<string, ExceptionCategoryGroup>>\n\n/**\n * One classification a record is evaluated against -- deliberately just `{ group, category }`,\n * never `domain`/`rule`/`severity`/`exceptionType` or any other consumer-specific vocabulary. A\n * consumer maps its own domain concepts onto this shape at the call site (e.g.\n * `suppression-governance`'s `{ group: record.domain, category: rule }` per suppressed rule;\n * `security-socket`'s `{ group: \"socket\", category: normalizedSeverity }`) -- the core itself\n * never interprets `group`/`category` beyond using them as lookup keys into an\n * `ExceptionPolicyConfig`.\n * @beta\n */\nexport interface ExceptionClassification {\n /** The top-level key this classification resolves against in an `ExceptionPolicyConfig`. */\n readonly group: string\n /** The `rules` key (exact or glob-matched) this classification resolves against within `group`. */\n readonly category: string\n}\n\n/**\n * A resolved judgment about one record, once its `ExceptionPolicy` has been checked against its\n * own field values. See `ExceptionDeterminant`.\n * @beta\n */\nexport type ExceptionVerdict = \"forbidden\" | \"insufficient\" | \"permitted\"\n\n/**\n * One record's resolved policy verdict, and (for `\"insufficient\"`) which required fields are\n * still empty. Never published as a batch/matched-vs-unmatched shape -- matching a finding to a\n * record stays entirely check-owned (each check reconciles its own findings against its own\n * registry via `reconcileExceptions`).\n * @beta\n */\nexport interface ExceptionDeterminant<TRecord> {\n /** The record this determinant was computed for, returned verbatim. */\n readonly record: TRecord\n /** `\"forbidden\"`: never permitted. `\"insufficient\"`: exception-eligible, but `missing` is non-empty. `\"permitted\"`: every required field is filled in (or the resolved policy was `\"allowed\"`). */\n readonly verdict: ExceptionVerdict\n /** Every required field (by name) still empty on `record`, in the resolved policy's own `requirements` order. Always `[]` for `\"forbidden\"`/`\"permitted\"`. */\n readonly missing: readonly string[]\n}\n\n/**\n * The stricter of two policies: `\"forbidden\"` always wins outright; between two non-forbidding\n * policies, the union of their required fields wins (an `\"allowed\"` policy contributes no\n * requirements, so merging it with an `\"exception\"` policy just yields that same exception\n * unchanged) -- satisfying the union trivially satisfies each individual input policy too. Reused\n * for two distinct combinations: multiple glob patterns matching the same category, and multiple\n * classifications resolved for the same record (see `evaluateExceptionRecord`). The union preserves\n * `a`'s own field order first, then appends any of `b`'s fields not already present -- deterministic\n * given deterministic inputs, without needing a fixed, closed field-name enum the way\n * `resolve-policy.ts`'s own `REQUIREMENT_ORDER` does (this core never owns a closed vocabulary of\n * field names; see `checks/shared/exception-record.ts` for where a consumer's own closed\n * `EXCEPTION_TYPES`-style enum lives instead).\n * @param a - One resolved policy.\n * @param b - The other resolved policy.\n * @returns The stricter of `a` and `b`.\n */\nexport function stricterOf(a: ExceptionPolicy, b: ExceptionPolicy): ExceptionPolicy {\n if (a.mode === \"forbidden\" || b.mode === \"forbidden\") return { mode: \"forbidden\" }\n\n const aRequirements = a.mode === \"exception\" ? a.requirements : []\n const bRequirements = b.mode === \"exception\" ? b.requirements : []\n\n if (aRequirements.length === 0 && bRequirements.length === 0) return { mode: \"allowed\" }\n\n const requirements = [...aRequirements]\n for (const requirement of bRequirements) {\n if (!requirements.includes(requirement)) requirements.push(requirement)\n }\n\n return { mode: \"exception\", requirements }\n}\n\n/**\n * Resolves the policy one `{ group, category }` classification is subject to, in strict precedence\n * order -- documented here as this function's one authoritative source of truth for that order\n * (generalized from `scripts/suppression-governance/resolve-policy.ts`'s own `resolveRequirement`,\n * which this function's future retrofit replaces):\n *\n * 0. **No entry for `group`** -- `config[group] === undefined` -- resolves to `globalDefault`\n * immediately, before `category` is even inspected (so a blanket `category === \"*\"` against an\n * unconfigured group still just returns `globalDefault`, never an empty-array reduce).\n * 1. **Blanket category** -- `category === \"*\"` means every category this group could ever apply\n * to was matched at once, not one specific category literally named `\"*\"`. It resolves as the\n * strictest (`stricterOf`) policy across every entry in `group.rules` plus the group's own\n * default (or `globalDefault`) -- never via `minimatch`: `minimatch(\"*\", pattern)` tests the\n * literal one-character string `\"*\"` as a path against `pattern`, which does not glob-match a\n * pattern like `\"security/*\"` (that would require the *pattern*, not the *target*, to be `\"*\"`),\n * so treating this case as an ordinary pattern match would silently let a blanket match fall\n * through a group's `forbidden` rules into its far more lenient default.\n * 2. **Exact match** -- `group.rules[category]`, if present. A glob is never even consulted once an\n * exact entry exists for `category`.\n * 3. **Glob match** -- the strictest (`stricterOf`) policy among every key in `group.rules` that is\n * not itself an exact match for `category` but does match it as a `minimatch` glob (e.g.\n * `\"security/*\"` matching `\"security/detect-object-injection\"`).\n * 4. **Group default** -- `group.default`, if `group` itself has an entry in `config` (whether or\n * not that entry defines its own `default`).\n * 5. **Global default** -- `globalDefault`, used only when `group` itself has no entry in `config`\n * at all, or when neither an exact/glob match nor a `group.default` applies.\n *\n * Assumes `config` has already passed `validateExceptionPolicyConfig`.\n * @param classification - The `{ group, category }` pair to resolve a policy for.\n * @param config - The exception policy configuration to resolve against.\n * @param globalDefault - The policy to fall back to when `classification.group` has no entry in `config` at all.\n * @returns The resolved policy for `classification`.\n * @beta\n */\nexport function resolveExceptionPolicy(\n classification: ExceptionClassification,\n config: ExceptionPolicyConfig,\n globalDefault: ExceptionPolicy,\n): ExceptionPolicy {\n const { group, category } = classification\n const groupPolicy = config[group]\n if (groupPolicy === undefined) return globalDefault\n\n const rules = Object.entries(groupPolicy.rules ?? {})\n\n if (category === \"*\") {\n const everyPolicy = [...rules.map(([, policy]) => policy), groupPolicy.default ?? globalDefault]\n return everyPolicy.reduce(stricterOf)\n }\n\n const exactMatch = rules.find(([pattern]) => pattern === category)\n if (exactMatch) return exactMatch[1]\n\n const globMatches = rules\n .filter(([pattern]) => {\n // Equivalent mutant: by the time this line runs, `exactMatch` above has already returned\n // for any entry whose `pattern` literally equals `category`, so no remaining entry in\n // `rules` can ever have `pattern === category` here -- `pattern !== category` is therefore\n // always `true` at this point, and no test could ever distinguish it from the literal\n // `true` a mutant substitutes for it.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n return pattern !== category && minimatch(category, pattern)\n })\n .map(([, policy]) => policy)\n if (globMatches.length > 0) {\n return globMatches.reduce(stricterOf)\n }\n\n return groupPolicy.default ?? globalDefault\n}\n\n/**\n * One record to evaluate, paired with everything `evaluateExceptionRecord` needs to judge it --\n * shared by `evaluateExceptionRecord` and `evaluateExceptionRecords` (whose own `inputs` is just\n * `readonly ExceptionRecordEvaluation<TRecord>[]`) so the same five-field shape isn't declared\n * twice.\n * @beta\n */\nexport interface ExceptionRecordEvaluation<TRecord> {\n /** The record to evaluate. */\n readonly record: TRecord\n /** Every classification this record is subject to; the strictest resolved policy across all of them wins. */\n readonly classifications: readonly [ExceptionClassification, ...ExceptionClassification[]]\n /** The exception policy configuration to resolve `classifications` against. */\n readonly config: ExceptionPolicyConfig\n /** The policy to fall back to for any classification whose `group` has no entry in `config` at all. */\n readonly globalDefault: ExceptionPolicy\n /** Resolves one named required field's current string value on `record`. */\n readonly fieldValue: (record: TRecord, requirement: string) => string\n}\n\n/**\n * Evaluates one record against every one of its own classifications, taking the strictest\n * (`stricterOf`) of each classification's resolved policy -- a record matching both a forbidden\n * classification and an otherwise-fine one is forbidden overall. `classifications` is a non-empty\n * tuple by type: zero classifications would be a caller bug (which classification would silently\n * fall back to `globalDefault`?), never a case this function has to guess about at runtime. A\n * required field only counts as satisfied once `fieldValue(record, requirement).trim()` is\n * non-empty -- an empty field is valid *data*, but policy-insufficient, exactly as `\"exception\"`\n * mode's name implies. `fieldValue` may resolve a dotted path (e.g. `\"verification.verifiedBy\"`)\n * or anything else a consumer's own record shape needs -- this function never interprets\n * `requirement` itself, it only ever calls `fieldValue(record, requirement)` and trims the result.\n * @param input - The record to evaluate, its classifications, the policy configuration and global default to resolve them against, and the field-value accessor -- see `ExceptionRecordEvaluation`'s own per-field doc comments.\n * @returns The record's verdict, and which required fields (if any) are still missing.\n * @beta\n */\nexport function evaluateExceptionRecord<TRecord>(\n input: ExceptionRecordEvaluation<TRecord>,\n): ExceptionDeterminant<TRecord> {\n const { record, classifications, config, globalDefault, fieldValue } = input\n\n const resolvedPolicy = classifications\n .map((classification) => resolveExceptionPolicy(classification, config, globalDefault))\n .reduce(stricterOf)\n\n if (resolvedPolicy.mode === \"forbidden\") {\n return { record, verdict: \"forbidden\", missing: [] }\n }\n\n const requirements = resolvedPolicy.mode === \"exception\" ? resolvedPolicy.requirements : []\n const missing = requirements.filter(\n (requirement) => fieldValue(record, requirement).trim().length === 0,\n )\n\n return { record, verdict: missing.length === 0 ? \"permitted\" : \"insufficient\", missing }\n}\n\n/**\n * A thin batch over already-matched `(record, classifications)` pairs -- exactly\n * `inputs.map(evaluateExceptionRecord)`, provided so a caller evaluating many records against the\n * same `config`/`globalDefault`/`fieldValue` doesn't have to write that `.map` itself. Matching a\n * record to a finding in the first place stays entirely check-owned (each check reconciles its own\n * findings against its own registry) -- this function takes already-paired inputs, it never does\n * any matching of its own.\n * @param inputs - Each already-matched record to evaluate, in the same shape `evaluateExceptionRecord` itself takes.\n * @returns Each input's own determinant, in the same order as `inputs`.\n * @beta\n */\nexport function evaluateExceptionRecords<TRecord>(\n inputs: readonly ExceptionRecordEvaluation<TRecord>[],\n): readonly ExceptionDeterminant<TRecord>[] {\n return inputs.map((input) => evaluateExceptionRecord(input))\n}\n\nconst VALID_MODES = [\"forbidden\", \"allowed\", \"exception\"] as const\n\n/**\n * Validates one `ExceptionPolicy` value's shape -- `mode` must be one of the three recognized\n * modes, and an `\"exception\"` mode's `requirements` must be a non-empty array (an empty array is\n * rejected rather than silently treated as equivalent to `\"allowed\"` -- if nothing is required,\n * `\"allowed\"` is the correct, unambiguous way to say so). When `validRequirements` is supplied,\n * every named requirement must also be a member of it.\n * @param value - The candidate policy value to validate.\n * @param location - Where this value lives in the configuration, for error messages.\n * @param validRequirements - The complete set of field names this consumer's policy may require, if the consumer wants that checked. Omit to skip this check entirely (any string is accepted as a requirement name).\n * @param errors - Accumulates every configuration problem found.\n */\nfunction validateExceptionPolicyValue(\n value: unknown,\n location: string,\n validRequirements: readonly string[] | undefined,\n errors: string[],\n): void {\n if (typeof value !== \"object\" || value === null) {\n errors.push(`${location} must be an object.`)\n return\n }\n\n const { mode, requirements } = value as Record<string, unknown>\n\n if (mode === \"forbidden\" || mode === \"allowed\") return\n\n if (mode !== \"exception\") {\n errors.push(\n `${location}.mode must be one of ${VALID_MODES.map((m) => `\"${m}\"`).join(\", \")} (got ${JSON.stringify(mode)}).`,\n )\n return\n }\n\n if (!Array.isArray(requirements) || requirements.length === 0) {\n errors.push(`${location}.requirements must be a non-empty array when mode is \"exception\".`)\n return\n }\n\n // Every requirement name must be a string regardless of whether `validRequirements` was\n // supplied -- `evaluateExceptionRecord`'s `fieldValue(record, requirement)` expects a string\n // key, and a decoded-but-unvalidated policy (e.g. straight from untrusted JSON) could otherwise\n // carry a number/object/null entry through to that call. `validRequirements`, when supplied,\n // additionally narrows to a specific allowed set; when omitted, \"is a string\" is still checked.\n for (const requirement of requirements) {\n if (typeof requirement !== \"string\") {\n errors.push(\n `${location}.requirements contains a non-string entry (got ${JSON.stringify(requirement)}).`,\n )\n continue\n }\n if (validRequirements !== undefined && !validRequirements.includes(requirement)) {\n errors.push(\n `${location}.requirements contains an invalid entry (got ${JSON.stringify(requirement)}); ` +\n `expected one of ${validRequirements.map((r) => `\"${r}\"`).join(\", \")}.`,\n )\n }\n }\n}\n\n/**\n * Whether `value` is a non-null, non-array object -- the shape every group's own entry in a\n * `ExceptionPolicyConfig`, and every group's `rules` container, is expected to be before its own\n * fields are inspected. `config`'s declared type (`ExceptionPolicyConfig`) does not, by itself,\n * guarantee this at runtime: this function exists to validate a config that may have been decoded\n * from untrusted JSON and only cast to that type, not actually shaped like it.\n * @param value - The candidate value to check.\n * @returns `true` if `value` is a plain object.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/**\n * Validates an entire `ExceptionPolicyConfig`'s shape -- every group's `default` and every entry\n * in its `rules` must be a well-formed `ExceptionPolicy` (see `validateExceptionPolicyValue`), and\n * no group's `rules` may use the literal `\"*\"` as a key. A literal `\"*\"` key is always a mistake,\n * never an intentional blanket policy: `resolveExceptionPolicy`'s own blanket-category handling is\n * triggered by the *input* `category` being `\"*\"`, not by a `\"*\"` entry in `rules` -- a `\"*\"` rules\n * key would instead be consulted only as an ordinary `minimatch` glob, which matches the literal\n * one-character string `\"*\"` as a *target*, not as a wildcard pattern matching every real category\n * name (`minimatch(\"*\", pattern)` truthiness depends on `pattern`, not the other way around) --\n * see `resolveExceptionPolicy`'s own doc comment, case 1, for the failure mode this prevents.\n * @param config - The exception policy configuration to validate.\n * @param validRequirements - The complete set of field names this consumer's policy may require, if the consumer wants that checked. Omit to skip that check entirely.\n * @returns Every configuration problem found; empty if `config` is valid.\n * @beta\n */\nexport function validateExceptionPolicyConfig(\n config: ExceptionPolicyConfig,\n validRequirements?: readonly string[],\n): readonly string[] {\n const errors: string[] = []\n\n for (const [group, groupPolicy] of Object.entries(config)) {\n // `groupPolicy`'s declared type (`ExceptionCategoryGroup`) does not guarantee this shape at\n // runtime -- a config decoded from untrusted JSON and merely cast to `ExceptionPolicyConfig`\n // could carry `null`, a string, or an array here. Reject it before dereferencing `.default`/\n // `.rules`, rather than throwing (`null.default`) or silently treating a non-object as an\n // empty group (both real failure modes this guard closes).\n if (!isPlainObject(groupPolicy)) {\n errors.push(`config.${group} must be an object.`)\n continue\n }\n\n if (groupPolicy.default !== undefined) {\n validateExceptionPolicyValue(\n groupPolicy.default,\n `config.${group}.default`,\n validRequirements,\n errors,\n )\n }\n\n if (groupPolicy.rules !== undefined && !isPlainObject(groupPolicy.rules)) {\n errors.push(`config.${group}.rules must be an object.`)\n continue\n }\n\n // No cast needed here: the guard above has already narrowed `groupPolicy.rules` to\n // `Record<string, unknown> | undefined` via `isPlainObject`'s type predicate.\n for (const [pattern, policy] of Object.entries(groupPolicy.rules ?? {})) {\n if (pattern === \"*\") {\n errors.push(\n `config.${group}.rules must not use the literal \"*\" as a key -- it would be consulted ` +\n 'only as an ordinary minimatch glob (matching the literal one-character category \"*\", ' +\n \"never every category in the group) rather than as the blanket policy \" +\n \"`resolveExceptionPolicy` already applies whenever the classification's own `category` \" +\n 'is \"*\". Omit this key, or use a more specific pattern.',\n )\n }\n validateExceptionPolicyValue(\n policy,\n `config.${group}.rules[\"${pattern}\"]`,\n validRequirements,\n errors,\n )\n }\n }\n\n return errors\n}\n\n/**\n * A deterministic digest of a set of fields' current values on `record`, via `node:crypto` --\n * pure, synchronous, no ambient state (Socket.dev has no alert on `node:crypto`; it is not\n * `child_process`/`process.env`, the two capabilities `src/helpers/**` must never touch -- see\n * `scripts/verify-no-ambient-capabilities.mjs`). This is what makes a \"verification\" *content-bound*\n * rather than merely attested: a consumer records this hash (see `ExceptionVerification` in\n * `checks/shared/exception-record.ts`) at the moment a human or a mechanical re-check approved a\n * record's current prose; recomputing it later and comparing is how staleness is detected with no\n * separate tracking logic -- edit any field named in `fields` and the hash silently stops matching.\n * Each field's name is bound into the digest alongside its value, both serialized via\n * `JSON.stringify` as a `[name, value]` pair -- JSON's own escaping makes the digest unambiguous\n * regardless of what characters a field's name or value contains, so two different field sets\n * whose values happen to concatenate identically as plain text can never collide here.\n * @param record - The record to hash fields from.\n * @param fields - Which fields (by name, in this exact order) to include in the digest -- the same names `fieldValue` would be called with by `evaluateExceptionRecord`.\n * @param fieldValue - Resolves one named field's current string value on `record`, exactly like `evaluateExceptionRecord`'s own `fieldValue` parameter.\n * @returns A hex-encoded SHA-256 digest of `fields`' current values.\n * @beta\n */\nexport function hashRequirementFields<TRecord>(\n record: TRecord,\n fields: readonly string[],\n fieldValue: (record: TRecord, requirement: string) => string,\n): string {\n const canonical = JSON.stringify(fields.map((field) => [field, fieldValue(record, field)]))\n // Equivalent mutant: Node's Hash.update(data, inputEncoding) treats a falsy/empty\n // inputEncoding identically to \"utf8\" for a string `data` argument -- confirmed directly:\n // createHash(\"sha256\").update(x, \"utf8\").digest(\"hex\") === createHash(\"sha256\").update(x,\n // \"\").digest(\"hex\") for every input tried. No test could ever distinguish \"utf8\" from \"\" at\n // this exact call site.\n // Stryker disable next-line StringLiteral -- equivalent mutant, see comment above.\n return createHash(\"sha256\").update(canonical, \"utf8\").digest(\"hex\")\n}\n","import { readFile as readFileFromFs } from \"node:fs/promises\"\nimport type { StandardSchemaV1 } from \"../standard-schema/types.js\"\n\n/**\n * Renders one failed-validation issue as `path: message`, or just `message` when it has no path --\n * the same shape `src/parsing/format-schema-issues.ts` produces for `output.schema` failures, kept\n * as an independent, much smaller copy here rather than an import: `src/helpers/**` is a second,\n * independent published barrel (see `src/presets/index.ts`'s own doc comment on the same point) and\n * deliberately never reaches into `src/parsing/`, an internal layer of the root barrel it has no\n * business depending on.\n * @param issue - one issue from a failed `StandardSchemaV1.Result`.\n * @returns the rendered issue.\n */\nfunction formatIssue(issue: StandardSchemaV1.Issue): string {\n if (issue.path === undefined || issue.path.length === 0) return issue.message\n\n const path = issue.path\n // eslint-disable-next-line secure-coding/no-improper-type-validation -- `segment` is declared `PropertyKey | StandardSchemaV1.PathSegment` (never `null` or an array), so `typeof segment === \"object\"` can only match the `PathSegment` object form here; the null/array-safe rewrite this rule suggests is flagged as an unreachable condition by @typescript-eslint/no-unnecessary-condition given that exact type, so satisfying both rules at once is impossible -- the type itself is the guarantee this check would otherwise add at runtime.\n .map((segment) => (typeof segment === \"object\" ? segment.key : segment))\n .map((key, index) => {\n if (typeof key === \"number\") return `[${String(key)}]`\n const rendered = String(key)\n return index === 0 ? rendered : `.${rendered}`\n })\n .join(\"\")\n\n return `${path}: ${issue.message}`\n}\n\n/**\n * Whether `value` is a non-`null`, non-array object -- the shape an exception registry's on-disk\n * envelope must be before its own `exceptions` field is inspected.\n * @param value - the candidate value to check.\n * @returns `true` if `value` is a plain object.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reads and validates one exception registry file from disk -- `path -> { \"$schema\"?: string,\n * \"exceptions\": T[] }`, in two independently-validated layers. This function owns only the\n * envelope: that the file exists (or is absent, a normal empty-registry state), parses as JSON,\n * and is an object carrying an `\"exceptions\"` field at all. It never inspects what is inside\n * `exceptions` beyond that -- `schema` (a `StandardSchemaV1`, hand-written or from a real library;\n * see `src/standard-schema/types.ts`) owns every field-level concern for the caller's own record\n * shape, exactly the same \"consumer supplies a trusted capability, this package calls it without\n * owning its internals\" relationship `RepoContractConfig.spawn`/`env` already establish (see\n * `specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md`).\n *\n * `readFile` defaults to `node:fs/promises`' own `readFile` -- already used by\n * `src/presets/security-secrets.ts`, so this introduces no new filesystem-access surface; a\n * missing file is a normal \"no exceptions recorded yet\" state, not an error (`{ ok: true, records:\n * [] }`), matching `scripts/suppression-governance/check.ts`'s own `loadExistingRegistry`\n * precedent for the same first-run case. Every other failure -- unreadable-for-another-reason,\n * malformed JSON, a missing/malformed envelope, or a schema validation failure -- returns `{ ok:\n * false, errors }` rather than throwing: a bad registry is reported as data for a check's policy to\n * fail on, never an uncaught exception that crashes the run. The one exception is `schema`'s own\n * `validate()` throwing or rejecting -- a bug in the *caller-supplied schema*, not malformed\n * registry data -- which is deliberately left to propagate as a rejected `Promise`, mirroring\n * `src/parsing/parse-output.ts`'s identical treatment of a throwing `output.schema`.\n * @param input - Where to read the registry from, the schema that validates its `exceptions` array, and (for tests, or a non-`node:fs` environment) an override for how to read `path`.\n * @param input.path - The registry file's path, passed to `input.readFile` verbatim.\n * @param input.schema - Validates (and may transform) the envelope's `exceptions` array once this function's own envelope checks pass.\n * @param input.readFile - Reads `input.path`'s content. Defaults to `node:fs/promises`' own `readFile(path, \"utf8\")`.\n * @returns Every valid record (`ok: true`), or every problem found reading/parsing/validating the file (`ok: false`).\n * @beta\n */\nexport async function loadExceptionRegistry<T>(input: {\n readonly path: string\n readonly schema: StandardSchemaV1<unknown, readonly T[]>\n readonly readFile?: (path: string) => Promise<string>\n}): Promise<{ ok: true; records: readonly T[] } | { ok: false; errors: readonly string[] }> {\n // Equivalent mutant, confirmed empirically: `node:fs/promises`'s `readFile(path, \"\")` returns\n // a raw `Buffer` rather than a decoded string (unlike `Hash.update`, an empty inputEncoding is\n // not treated the same as \"utf8\" here) -- but this function's only use of `raw` is\n // `JSON.parse(raw)` immediately below, and `JSON.parse`'s own `ToString` coercion on a `Buffer`\n // calls `Buffer.prototype.toString()` with no arguments, whose own default encoding is \"utf8\"\n // -- confirmed directly, including with multi-byte UTF-8 content, that\n // `JSON.parse(bufferReadWithEmptyEncoding)` produces byte-identical results to\n // `JSON.parse(stringReadWithUtf8Encoding)` every time. No test could ever observe a difference\n // through this function's own return value.\n // Stryker disable next-line StringLiteral -- equivalent mutant, see comment above.\n const { path, schema, readFile = (target: string) => readFileFromFs(target, \"utf8\") } = input\n\n let raw: string\n try {\n raw = await readFile(path)\n } catch (error) {\n // `readFile` is a caller-supplied capability (see this function's own doc comment) -- a test\n // override, or an unusual real filesystem implementation, could reject with something other\n // than a real `Error` (`null`, a plain string, ...); reading `.code`/`.message` off that\n // directly would throw out of this catch block instead of returning the clean `{ ok: false }`\n // this function promises for every other failure. `isPlainObject` narrows first.\n // Equivalent mutant: `code` is consumed only by the `code === \"ENOENT\"` comparison two lines\n // down, which requires an exact primitive-string match -- there is no value for which\n // `typeof error.code === \"string\"` is false yet `error.code === \"ENOENT\"` is true (a value\n // that literally equals the primitive string \"ENOENT\" always has `typeof` \"string\"). Mutating\n // this `typeof` check to `true` therefore changes what gets assigned to `code` for a\n // non-string `.code` (e.g. a number, or `undefined`), but never changes whether the\n // subsequent `=== \"ENOENT\"` comparison can succeed -- confirmed directly: every plain-object\n // test case covering this line (a numeric code, an absent code) produces the identical\n // not-ENOENT branch and final message either way.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n const code = isPlainObject(error) && typeof error.code === \"string\" ? error.code : undefined\n if (code === \"ENOENT\") return { ok: true, records: [] }\n const message =\n isPlainObject(error) && typeof error.message === \"string\" ? error.message : String(error)\n return { ok: false, errors: [`Could not read ${path}: ${message}`] }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (error) {\n return { ok: false, errors: [`${path} is not valid JSON: ${(error as Error).message}`] }\n }\n\n if (!isPlainObject(parsed)) {\n return {\n ok: false,\n errors: [`${path} must contain a JSON object with an \"exceptions\" array (got a non-object).`],\n }\n }\n\n const { exceptions } = parsed\n if (exceptions === undefined) {\n return { ok: false, errors: [`${path} is missing its required \"exceptions\" field.`] }\n }\n\n const result = await schema[\"~standard\"].validate(exceptions)\n if (result.issues !== undefined) {\n return { ok: false, errors: result.issues.map((issue) => formatIssue(issue)) }\n }\n\n return { ok: true, records: result.value }\n}\n","/**\n * The registry-lifecycle half of `repo-contract/helpers` -- `reconcileExceptions` diffs a check's\n * raw findings against its already-loaded exception registry, and `serializeExceptionRegistry`\n * renders the reconciled records back to their canonical on-disk form. Both are pure; the only\n * I/O is `writeExceptionRegistry` in its own module. See\n * specs/decisions/0013-reusable-exception-policy-helper.md's \"The exception registry is the review\n * surface\" section for the model these serve: a check emits 100% of its findings, reconciliation\n * maintains one record per finding (a fresh stub for a new one), and stale records are surfaced,\n * never removed.\n */\n\n/**\n * The three fields every exception record in `.repo-contract/exceptions/*.json` carries, whatever\n * the owning check. A registry adds its own typed fields on top; this is the shared core the\n * generic machinery (`reconcileExceptions`, `serializeExceptionRegistry`,\n * `validateExceptionRegistry` in the check-owned layer) relies on.\n * @beta\n */\nexport interface ExceptionRecordCore {\n /** The check-namespaced semantic identity of the finding this record waives (e.g. `\"suppression:eslint:no-console:src/foo.ts:<module>\"`). Equals the finding id; a record whose id matches no current finding is stale. Never derived from prose. */\n readonly id: string\n /** Record-schema version. */\n readonly version: number\n /** The one human-authored field: why this guardrail is deliberately bypassed, and what was checked to confirm the finding is real. `\"\"` in a freshly scaffolded stub (the policy, not the validator, rejects a blank/placeholder value). */\n readonly justification: string\n}\n\n/**\n * The outcome of reconciling one run's findings against one exception registry.\n * @beta\n */\nexport interface ExceptionReconciliation<TFinding, TRecord> {\n /** Each finding paired with the existing record it matched, in `findings` order. */\n readonly matchedPairs: readonly { readonly finding: TFinding; readonly record: TRecord }[]\n /** Every record to persist as live: matched records verbatim, plus one fresh `createStub` per unmatched finding. Never contains a stale record. */\n readonly activeRecords: readonly TRecord[]\n /** Existing records whose id matched no finding this run -- surfaced for the policy to fail on, **never removed** by this function (retiring one is an explicit human edit). */\n readonly staleRecords: readonly TRecord[]\n /** The ids of the stubs created this run -- a subset of `activeRecords`' ids. */\n readonly newStubIds: readonly string[]\n}\n\n/**\n * Reconciles this run's raw findings against the check's already-loaded, already-validated\n * exception registry. Pure and add-only: it never mutates a matched record and never removes a\n * stale one.\n *\n * `deriveId` must be **injective** over `findings` -- every independently-governable finding needs\n * a distinct id, or the mechanism cannot tell which finding a record's `justification` belongs to.\n * A collision is returned as `{ ok: false }` (a real condition a check must surface: the source\n * genuinely holds two identical directives), naming both finding indices so the check can point a\n * developer at them.\n *\n * `createStub(finding, id)` is handed the canonical id and its result's `id` is asserted equal to\n * it -- a `createStub` that derives its own identity differently is a `{ ok: false }` bug report,\n * never a silently-mismatched record.\n *\n * Precondition: `existing` has already passed the check's registry validator (unique ids, correct\n * namespace). This function does not re-validate it.\n * @param input - The already-loaded registry, this run's findings, and the check-owned identity + stub callbacks.\n * @param input.existing - The validated records currently on disk.\n * @param input.findings - Every raw finding this run produced -- nothing filtered.\n * @param input.deriveId - The finding's check-namespaced semantic id. Must be injective over `findings`.\n * @param input.createStub - Builds a fresh record for an unmatched finding; receives the canonical id and must return a record carrying it.\n * @returns The reconciliation, or the first integrity problem found.\n * @beta\n */\nexport function reconcileExceptions<TFinding, TRecord extends { readonly id: string }>(input: {\n readonly existing: readonly TRecord[]\n readonly findings: readonly TFinding[]\n readonly deriveId: (finding: TFinding) => string\n readonly createStub: (finding: TFinding, id: string) => TRecord\n}):\n | { readonly ok: true; readonly reconciliation: ExceptionReconciliation<TFinding, TRecord> }\n | { readonly ok: false; readonly error: string } {\n const { existing, findings, deriveId, createStub } = input\n\n const idByFirstIndex = new Map<string, number>()\n const identified: { readonly finding: TFinding; readonly id: string }[] = []\n for (const [index, finding] of findings.entries()) {\n const id = deriveId(finding)\n const prior = idByFirstIndex.get(id)\n if (prior !== undefined) {\n return {\n ok: false,\n error: `deriveId is not injective: findings at index ${String(prior)} and ${String(index)} both map to id ${JSON.stringify(id)} -- every independently-governable finding must have a distinct id; make the two directives distinguishable.`,\n }\n }\n idByFirstIndex.set(id, index)\n identified.push({ finding, id })\n }\n\n const existingById = new Map<string, TRecord>()\n for (const record of existing) existingById.set(record.id, record)\n\n const matchedPairs: { readonly finding: TFinding; readonly record: TRecord }[] = []\n const activeRecords: TRecord[] = []\n const newStubIds: string[] = []\n\n for (const { finding, id } of identified) {\n const match = existingById.get(id)\n if (match !== undefined) {\n matchedPairs.push({ finding, record: match })\n activeRecords.push(match)\n continue\n }\n const stub = createStub(finding, id)\n if (stub.id !== id) {\n return {\n ok: false,\n error: `createStub returned a record whose id ${JSON.stringify(stub.id)} does not equal the canonical id ${JSON.stringify(id)} it was given.`,\n }\n }\n activeRecords.push(stub)\n newStubIds.push(id)\n }\n\n const liveIds = new Set(idByFirstIndex.keys())\n const staleRecords = existing.filter((record) => !liveIds.has(record.id))\n\n return {\n ok: true,\n reconciliation: { matchedPairs, activeRecords, staleRecords, newStubIds },\n }\n}\n\n/**\n * Rebuilds one record with a canonical key order: `id`, `version`, `justification`, then every\n * other own key in code-unit order. Records are flat (scalars and scalar arrays); if a registry\n * ever adds a nested metadata object, its inner keys keep their insertion order until this\n * function is extended. Keys are copied with `Object.defineProperty` so an own `\"__proto__\"` key\n * is carried through as a plain data property rather than silently dropped by the setter (and the\n * prototype is never mutated).\n * @param record - The record to reorder.\n * @returns A new object with the same entries in canonical key order.\n */\nfunction canonicalRecord(\n record: Record<string, unknown> & { readonly id: string },\n): Record<string, unknown> {\n const { id, version, justification, ...rest } = record\n const ordered: Record<string, unknown> = { id, version, justification }\n for (const key of Object.keys(rest).sort()) {\n // `Object.defineProperty`, not `ordered[key] = ...`, so an own `\"__proto__\"` key is stored as\n // a plain enumerable data property instead of being swallowed by the prototype setter. The\n // resulting object is only serialized and discarded, so leaving `writable`/`configurable` at\n // their `false` defaults is fine.\n Object.defineProperty(ordered, key, { value: rest[key], enumerable: true })\n }\n return ordered\n}\n\n/**\n * The canonical on-disk form of one exception registry: `{ \"exceptions\": [ ... ] }`, records\n * sorted ascending by `id` (code-unit order, locale-independent), each record's keys in\n * `canonicalRecord` order, 2-space indent, `\\n` line endings, a trailing newline. Fully\n * deterministic: the same records always serialize byte-for-byte identically, so a reconcile that\n * changes nothing produces no diff and `writeExceptionRegistry` writes nothing.\n *\n * Assumes `records` have unique ids (the caller reconciled them and validated its registry).\n * @param records - The reconciled records (`activeRecords` plus any `staleRecords`, in any order).\n * @returns The exact file contents to persist.\n * @beta\n */\nexport function serializeExceptionRegistry(\n records: readonly (Record<string, unknown> & { readonly id: string })[],\n): string {\n // `records` have unique ids by precondition, so the `<`/`<=` and `>`/`>=` mutants of this\n // comparator, and its `: 0` (tie) branch, are all equivalent -- no two ids ever compare equal.\n // The reversing mutants (swapped `-1`/`1`, the `true`/`false` conditional replacements) are\n // killed by the \"byte-identical regardless of record order\" property test, which serializes a\n // list and its reverse and requires identical bytes. Comparing the `id` strings directly (not\n // the records) also keeps this safe for null-prototype record objects.\n // Stryker disable next-line ConditionalExpression,EqualityOperator -- equivalent mutants, see comment above.\n const sorted = [...records].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n const canonical = sorted.map((record) => canonicalRecord(record))\n return `${JSON.stringify({ exceptions: canonical }, null, 2)}\\n`\n}\n","import {\n lstat as lstatFromFs,\n readFile as readFileFromFs,\n rename as renameFromFs,\n writeFile as writeFileFromFs,\n} from \"node:fs/promises\"\nimport { randomUUID } from \"node:crypto\"\nimport { serializeExceptionRegistry } from \"./reconcile-exceptions.js\"\n\n/**\n * Whether `error` carries `code === \"ENOENT\"` -- a missing file, a normal state the symlink probe\n * and the \"already up to date\" comparison both treat as \"no file yet\", never a failure. Reads\n * `.code` defensively: every filesystem capability here is caller-overridable and could reject\n * with something other than a real `Error`.\n * @param error - the caught value.\n * @returns `true` for an `ENOENT`.\n */\nfunction isEnoent(error: unknown): boolean {\n return (\n // Replacing this `typeof` guard with `true` still yields `false` for every non-object value:\n // a primitive's `.code` is `undefined` (never the string \"ENOENT\"), and `null` is caught by\n // the `!== null` clause. Same accepted equivalent mutant as load-exception-registry.ts's own\n // ENOENT check.\n // Stryker disable next-line ConditionalExpression -- equivalent mutant, see comment above.\n typeof error === \"object\" &&\n error !== null &&\n (error as { readonly code?: unknown }).code === \"ENOENT\"\n )\n}\n\n/**\n * Writes one exception registry to disk in its canonical form (`serializeExceptionRegistry`),\n * atomically and idempotently -- the sole filesystem-writing primitive in `repo-contract/helpers`.\n *\n * - **Symlinked target** -> `{ ok: false }`: a governance registry is never a symlink, and writing\n * through one would escape `.repo-contract/exceptions/`.\n * - **No file yet** -> written.\n * - **On-disk bytes already equal the canonical form** -> `{ ok: true, written: false }`, no\n * write. A reconcile that changed nothing therefore never dirties the working tree.\n * - **Different** -> the canonical bytes are written to a sibling temp file (`<path>.<uuid>.tmp`)\n * and `rename`d over the target -- the target is replaced atomically, and partial serialized\n * content is never written to the target path. A `rename` failure (e.g. a Windows lock on the\n * target) returns `{ ok: false }` deterministically; the temp file is left in place (harmless, a\n * uuid name, and the run has already failed) -- `.gitignore` covers `.repo-contract/exceptions/*.tmp`.\n *\n * Path containment is the caller's responsibility: repo-contract's own checks always pass\n * `.repo-contract/exceptions/<name>.json`. `readFile`/`writeFile`/`rename`/`isSymlink` default to\n * `node:fs/promises` and are overridable for tests or a non-`node:fs` environment, mirroring\n * `loadExceptionRegistry`'s own `readFile` parameter.\n * @param input - The target path, the records to persist, and (optional) filesystem-capability overrides.\n * @param input.path - Where to write. Passed verbatim to every capability.\n * @param input.records - The reconciled records; serialized via `serializeExceptionRegistry`.\n * @param input.readFile - Reads the current file. Default: `node:fs/promises` `readFile(path, \"utf8\")`.\n * @param input.writeFile - Writes the temp file. Default: `node:fs/promises` `writeFile(path, data, \"utf8\")`.\n * @param input.rename - Replaces the target with the temp file. Default: `node:fs/promises` `rename`.\n * @param input.isSymlink - Whether `path` is a symlink. Default: `lstat(path).isSymbolicLink()`, treating a missing path as not a symlink.\n * @returns `{ ok: true, written }` on success, or `{ ok: false, error }` for a symlink, an unreadable target, or a failed write/rename.\n * @beta\n */\nexport async function writeExceptionRegistry(input: {\n readonly path: string\n readonly records: readonly (Record<string, unknown> & { readonly id: string })[]\n readonly readFile?: (path: string) => Promise<string>\n readonly writeFile?: (path: string, data: string) => Promise<void>\n readonly rename?: (from: string, to: string) => Promise<void>\n readonly isSymlink?: (path: string) => Promise<boolean>\n}): Promise<\n { readonly ok: true; readonly written: boolean } | { readonly ok: false; readonly error: string }\n> {\n const {\n path,\n records,\n // No explicit encoding on either call: `readFile` returns a Buffer that `.toString()` decodes\n // as utf8 by default, and `writeFile` writes a string as utf8 by default -- so there is no\n // encoding literal for a mutant to flip.\n readFile = (target: string) => readFileFromFs(target).then((buffer) => buffer.toString()),\n writeFile = (target: string, data: string) => writeFileFromFs(target, data),\n rename = (from: string, to: string) => renameFromFs(from, to),\n isSymlink = defaultIsSymlink,\n } = input\n\n if (await isSymlink(path)) {\n return {\n ok: false,\n error: `${path} is a symlink; an exception registry must be a regular file inside .repo-contract/exceptions/.`,\n }\n }\n\n const serialized = serializeExceptionRegistry(records)\n\n let current: string | undefined\n try {\n current = await readFile(path)\n } catch (error) {\n if (!isEnoent(error)) {\n return { ok: false, error: `Could not read ${path}: ${String(error)}` }\n }\n }\n if (current === serialized) return { ok: true, written: false }\n\n const tempPath = `${path}.${randomUUID()}.tmp`\n try {\n await writeFile(tempPath, serialized)\n } catch (error) {\n return { ok: false, error: `Could not write ${tempPath}: ${String(error)}` }\n }\n\n try {\n await rename(tempPath, path)\n } catch (error) {\n return {\n ok: false,\n error: `Could not replace ${path} with the reconciled registry (temp file left at ${tempPath}): ${String(error)}`,\n }\n }\n\n return { ok: true, written: true }\n}\n\n/**\n * The default `isSymlink`: `lstat(path).isSymbolicLink()`, with a missing path reported as not a\n * symlink. A non-`ENOENT` `lstat` failure propagates -- a genuinely unreadable path is not\n * silently treated as safe.\n * @param target - the path to probe.\n * @returns whether `target` is a symbolic link.\n */\nasync function defaultIsSymlink(target: string): Promise<boolean> {\n try {\n return (await lstatFromFs(target)).isSymbolicLink()\n } catch (error) {\n // A missing path is \"not a symlink\"; any other `lstat` failure (a non-directory path\n // component -> ENOTDIR, a permissions fault -> EACCES) is rethrown, never silently treated as\n // a safe non-symlink.\n if (isEnoent(error)) return false\n throw error\n }\n}\n"]}