@clossys/launcher 0.3.1 → 0.4.0

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 (285) hide show
  1. package/README.md +1324 -60
  2. package/contracts/conversation-contract.md +2 -2
  3. package/contracts/product-ci-workflow.yml +74 -0
  4. package/contracts/repository-inventory.json +53 -0
  5. package/dist/admission-fixture.d.ts +168 -0
  6. package/dist/admission-fixture.d.ts.map +1 -0
  7. package/dist/admission-fixture.js +453 -0
  8. package/dist/admission-fixture.js.map +1 -0
  9. package/dist/admission.d.ts +124 -0
  10. package/dist/admission.d.ts.map +1 -0
  11. package/dist/admission.js +799 -0
  12. package/dist/admission.js.map +1 -0
  13. package/dist/agents-guide.d.ts +9 -0
  14. package/dist/agents-guide.d.ts.map +1 -0
  15. package/dist/agents-guide.js +26 -0
  16. package/dist/agents-guide.js.map +1 -0
  17. package/dist/apply-command-options.check.d.ts +12 -0
  18. package/dist/apply-command-options.check.d.ts.map +1 -0
  19. package/dist/apply-command-options.check.js +20 -0
  20. package/dist/apply-command-options.check.js.map +1 -0
  21. package/dist/apply-plan-cli.d.ts +39 -1
  22. package/dist/apply-plan-cli.d.ts.map +1 -1
  23. package/dist/apply-plan-cli.js +432 -15
  24. package/dist/apply-plan-cli.js.map +1 -1
  25. package/dist/apply-plan.d.ts +46 -59
  26. package/dist/apply-plan.d.ts.map +1 -1
  27. package/dist/apply-plan.js +112 -97
  28. package/dist/apply-plan.js.map +1 -1
  29. package/dist/apply-step-fixture.d.ts +87 -0
  30. package/dist/apply-step-fixture.d.ts.map +1 -0
  31. package/dist/apply-step-fixture.js +199 -0
  32. package/dist/apply-step-fixture.js.map +1 -0
  33. package/dist/apply-store.d.ts +93 -0
  34. package/dist/apply-store.d.ts.map +1 -0
  35. package/dist/apply-store.js +625 -0
  36. package/dist/apply-store.js.map +1 -0
  37. package/dist/approval-sheet.d.ts +21 -0
  38. package/dist/approval-sheet.d.ts.map +1 -0
  39. package/dist/approval-sheet.js +157 -0
  40. package/dist/approval-sheet.js.map +1 -0
  41. package/dist/body-command.d.ts +42 -0
  42. package/dist/body-command.d.ts.map +1 -0
  43. package/dist/body-command.js +143 -0
  44. package/dist/body-command.js.map +1 -0
  45. package/dist/change-set-contract.d.ts +381 -0
  46. package/dist/change-set-contract.d.ts.map +1 -0
  47. package/dist/change-set-contract.js +738 -0
  48. package/dist/change-set-contract.js.map +1 -0
  49. package/dist/change-set-digest.d.ts +28 -0
  50. package/dist/change-set-digest.d.ts.map +1 -0
  51. package/dist/change-set-digest.js +65 -0
  52. package/dist/change-set-digest.js.map +1 -0
  53. package/dist/check-cli.d.ts.map +1 -1
  54. package/dist/check-cli.js +14 -3
  55. package/dist/check-cli.js.map +1 -1
  56. package/dist/cli.d.ts +17 -6
  57. package/dist/cli.d.ts.map +1 -1
  58. package/dist/cli.js +84 -23
  59. package/dist/cli.js.map +1 -1
  60. package/dist/core.d.ts +79 -22
  61. package/dist/core.d.ts.map +1 -1
  62. package/dist/core.js +843 -268
  63. package/dist/core.js.map +1 -1
  64. package/dist/dry-materialize.d.ts +63 -0
  65. package/dist/dry-materialize.d.ts.map +1 -0
  66. package/dist/dry-materialize.js +330 -0
  67. package/dist/dry-materialize.js.map +1 -0
  68. package/dist/generated/contract-schema.generated.d.ts +97 -0
  69. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  70. package/dist/generated/contract-schema.generated.js +496 -0
  71. package/dist/generated/contract-schema.generated.js.map +1 -0
  72. package/dist/generated/package-scope.generated.d.ts +6 -0
  73. package/dist/generated/package-scope.generated.d.ts.map +1 -0
  74. package/dist/generated/package-scope.generated.js +10 -0
  75. package/dist/generated/package-scope.generated.js.map +1 -0
  76. package/dist/generated/plan-contracts.generated.d.ts +3 -0
  77. package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
  78. package/dist/generated/plan-contracts.generated.js +2840 -0
  79. package/dist/generated/plan-contracts.generated.js.map +1 -0
  80. package/dist/host.d.ts.map +1 -1
  81. package/dist/host.js +11 -0
  82. package/dist/host.js.map +1 -1
  83. package/dist/identity.d.ts +15 -0
  84. package/dist/identity.d.ts.map +1 -0
  85. package/dist/identity.js +48 -0
  86. package/dist/identity.js.map +1 -0
  87. package/dist/index.d.ts +34 -5
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +19 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/inventory-adoption.d.ts +24 -5
  92. package/dist/inventory-adoption.d.ts.map +1 -1
  93. package/dist/inventory-adoption.js +70 -25
  94. package/dist/inventory-adoption.js.map +1 -1
  95. package/dist/inventory-choice.d.ts +40 -0
  96. package/dist/inventory-choice.d.ts.map +1 -0
  97. package/dist/inventory-choice.js +156 -0
  98. package/dist/inventory-choice.js.map +1 -0
  99. package/dist/inventory-contract.d.ts +89 -0
  100. package/dist/inventory-contract.d.ts.map +1 -0
  101. package/dist/inventory-contract.js +121 -0
  102. package/dist/inventory-contract.js.map +1 -0
  103. package/dist/key-editor.d.ts +30 -0
  104. package/dist/key-editor.d.ts.map +1 -0
  105. package/dist/key-editor.js +445 -0
  106. package/dist/key-editor.js.map +1 -0
  107. package/dist/ledger-contract.d.ts +187 -0
  108. package/dist/ledger-contract.d.ts.map +1 -0
  109. package/dist/ledger-contract.js +532 -0
  110. package/dist/ledger-contract.js.map +1 -0
  111. package/dist/ledger-trust.d.ts +90 -0
  112. package/dist/ledger-trust.d.ts.map +1 -0
  113. package/dist/ledger-trust.js +198 -0
  114. package/dist/ledger-trust.js.map +1 -0
  115. package/dist/lockfile-invariants.d.ts +48 -0
  116. package/dist/lockfile-invariants.d.ts.map +1 -0
  117. package/dist/lockfile-invariants.js +375 -0
  118. package/dist/lockfile-invariants.js.map +1 -0
  119. package/dist/lockfile-readers.d.ts +72 -0
  120. package/dist/lockfile-readers.d.ts.map +1 -0
  121. package/dist/lockfile-readers.js +713 -0
  122. package/dist/lockfile-readers.js.map +1 -0
  123. package/dist/lockfile-regen.d.ts +106 -0
  124. package/dist/lockfile-regen.d.ts.map +1 -0
  125. package/dist/lockfile-regen.js +760 -0
  126. package/dist/lockfile-regen.js.map +1 -0
  127. package/dist/lockfile-tool-env.d.ts +29 -0
  128. package/dist/lockfile-tool-env.d.ts.map +1 -0
  129. package/dist/lockfile-tool-env.js +111 -0
  130. package/dist/lockfile-tool-env.js.map +1 -0
  131. package/dist/materialize.d.ts +113 -0
  132. package/dist/materialize.d.ts.map +1 -0
  133. package/dist/materialize.js +840 -0
  134. package/dist/materialize.js.map +1 -0
  135. package/dist/observe-repository.d.ts +90 -0
  136. package/dist/observe-repository.d.ts.map +1 -0
  137. package/dist/observe-repository.js +1367 -0
  138. package/dist/observe-repository.js.map +1 -0
  139. package/dist/plan-bundle-setup-fixture.d.ts +68 -0
  140. package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
  141. package/dist/plan-bundle-setup-fixture.js +167 -0
  142. package/dist/plan-bundle-setup-fixture.js.map +1 -0
  143. package/dist/plan-bundle.d.ts +250 -0
  144. package/dist/plan-bundle.d.ts.map +1 -0
  145. package/dist/plan-bundle.js +827 -0
  146. package/dist/plan-bundle.js.map +1 -0
  147. package/dist/plan-command.d.ts +29 -0
  148. package/dist/plan-command.d.ts.map +1 -0
  149. package/dist/plan-command.js +493 -0
  150. package/dist/plan-command.js.map +1 -0
  151. package/dist/plan-contract.d.ts +153 -0
  152. package/dist/plan-contract.d.ts.map +1 -0
  153. package/dist/plan-contract.js +61 -0
  154. package/dist/plan-contract.js.map +1 -0
  155. package/dist/plan-digest.d.ts +25 -0
  156. package/dist/plan-digest.d.ts.map +1 -0
  157. package/dist/plan-digest.js +106 -0
  158. package/dist/plan-digest.js.map +1 -0
  159. package/dist/plan-rules.d.ts +23 -0
  160. package/dist/plan-rules.d.ts.map +1 -0
  161. package/dist/plan-rules.js +177 -0
  162. package/dist/plan-rules.js.map +1 -0
  163. package/dist/planned-bundle.d.ts +20 -0
  164. package/dist/planned-bundle.d.ts.map +1 -0
  165. package/dist/planned-bundle.js +191 -0
  166. package/dist/planned-bundle.js.map +1 -0
  167. package/dist/product-repository.d.ts +4 -0
  168. package/dist/product-repository.d.ts.map +1 -1
  169. package/dist/product-repository.js +9 -1
  170. package/dist/product-repository.js.map +1 -1
  171. package/dist/provenance-gate.d.ts +48 -0
  172. package/dist/provenance-gate.d.ts.map +1 -0
  173. package/dist/provenance-gate.js +324 -0
  174. package/dist/provenance-gate.js.map +1 -0
  175. package/dist/pull-request-body.d.ts +45 -0
  176. package/dist/pull-request-body.d.ts.map +1 -0
  177. package/dist/pull-request-body.js +232 -0
  178. package/dist/pull-request-body.js.map +1 -0
  179. package/dist/registry-snapshot.d.ts +141 -0
  180. package/dist/registry-snapshot.d.ts.map +1 -0
  181. package/dist/registry-snapshot.js +483 -0
  182. package/dist/registry-snapshot.js.map +1 -0
  183. package/dist/release-age-edit.d.ts +52 -0
  184. package/dist/release-age-edit.d.ts.map +1 -0
  185. package/dist/release-age-edit.js +413 -0
  186. package/dist/release-age-edit.js.map +1 -0
  187. package/dist/root-entries.d.ts +36 -0
  188. package/dist/root-entries.d.ts.map +1 -0
  189. package/dist/root-entries.js +80 -0
  190. package/dist/root-entries.js.map +1 -0
  191. package/dist/setup-template-scripts.d.ts +34 -0
  192. package/dist/setup-template-scripts.d.ts.map +1 -0
  193. package/dist/setup-template-scripts.js +557 -0
  194. package/dist/setup-template-scripts.js.map +1 -0
  195. package/dist/setup-templates.d.ts +54 -0
  196. package/dist/setup-templates.d.ts.map +1 -0
  197. package/dist/setup-templates.js +427 -0
  198. package/dist/setup-templates.js.map +1 -0
  199. package/dist/skills.d.ts +34 -1
  200. package/dist/skills.d.ts.map +1 -1
  201. package/dist/skills.js +129 -17
  202. package/dist/skills.js.map +1 -1
  203. package/dist/status.d.ts +63 -0
  204. package/dist/status.d.ts.map +1 -0
  205. package/dist/status.js +539 -0
  206. package/dist/status.js.map +1 -0
  207. package/dist/types.d.ts +151 -13
  208. package/dist/types.d.ts.map +1 -1
  209. package/package.json +4 -4
  210. package/skeleton/README.md +14 -9
  211. package/skeleton/package.json +2 -1
  212. package/skill/SKILL.md +23 -7
  213. package/skill-catalogue/advisor/SKILL.md +59 -6
  214. package/skill-catalogue/architect/SKILL.md +2 -2
  215. package/skill-catalogue/bouncer/SKILL.md +2 -2
  216. package/skill-catalogue/builder/SKILL.md +2 -2
  217. package/skill-catalogue/butler/SKILL.md +2 -2
  218. package/skill-catalogue/controller/SKILL.md +2 -2
  219. package/skill-catalogue/customer/SKILL.md +2 -2
  220. package/skill-catalogue/designer/SKILL.md +4 -2
  221. package/skill-catalogue/giver/SKILL.md +2 -2
  222. package/skill-catalogue/influencer/SKILL.md +2 -2
  223. package/skill-catalogue/inspector/SKILL.md +2 -2
  224. package/skill-catalogue/integrator/SKILL.md +2 -2
  225. package/skill-catalogue/keeper/SKILL.md +2 -2
  226. package/skill-catalogue/launcher/SKILL.md +23 -7
  227. package/skill-catalogue/locksmith/SKILL.md +2 -2
  228. package/skill-catalogue/messenger/SKILL.md +2 -2
  229. package/skill-catalogue/observer/SKILL.md +2 -2
  230. package/skill-catalogue/publisher/SKILL.md +2 -2
  231. package/skill-catalogue/starter/SKILL.md +3 -2
  232. package/skill-catalogue/strategist/SKILL.md +12 -4
  233. package/skill-catalogue/writer/SKILL.md +2 -2
  234. package/src/admission-fixture.ts +572 -0
  235. package/src/admission.ts +816 -0
  236. package/src/agents-guide.ts +29 -0
  237. package/src/apply-command-options.check.ts +27 -0
  238. package/src/apply-plan-cli.ts +454 -14
  239. package/src/apply-plan.ts +112 -124
  240. package/src/apply-step-fixture.ts +236 -0
  241. package/src/apply-store.ts +584 -0
  242. package/src/approval-sheet.ts +164 -0
  243. package/src/body-command.ts +162 -0
  244. package/src/change-set-contract.ts +937 -0
  245. package/src/change-set-digest.ts +70 -0
  246. package/src/check-cli.ts +14 -3
  247. package/src/cli.ts +90 -22
  248. package/src/core.ts +973 -275
  249. package/src/dry-materialize.ts +353 -0
  250. package/src/generated/contract-schema.generated.ts +520 -0
  251. package/src/generated/package-scope.generated.ts +10 -0
  252. package/src/generated/plan-contracts.generated.ts +2840 -0
  253. package/src/host.ts +10 -0
  254. package/src/identity.ts +51 -0
  255. package/src/index.ts +72 -3
  256. package/src/inventory-adoption.ts +107 -29
  257. package/src/inventory-choice.ts +172 -0
  258. package/src/inventory-contract.ts +166 -0
  259. package/src/key-editor.ts +446 -0
  260. package/src/ledger-contract.ts +637 -0
  261. package/src/ledger-trust.ts +267 -0
  262. package/src/lockfile-invariants.ts +421 -0
  263. package/src/lockfile-readers.ts +749 -0
  264. package/src/lockfile-regen.ts +851 -0
  265. package/src/lockfile-tool-env.ts +131 -0
  266. package/src/materialize.ts +886 -0
  267. package/src/observe-repository.ts +1365 -0
  268. package/src/plan-bundle-setup-fixture.ts +200 -0
  269. package/src/plan-bundle.ts +964 -0
  270. package/src/plan-command.ts +509 -0
  271. package/src/plan-contract.ts +179 -0
  272. package/src/plan-digest.ts +102 -0
  273. package/src/plan-rules.ts +188 -0
  274. package/src/planned-bundle.ts +211 -0
  275. package/src/product-repository.ts +10 -1
  276. package/src/provenance-gate.ts +352 -0
  277. package/src/pull-request-body.ts +261 -0
  278. package/src/registry-snapshot.ts +534 -0
  279. package/src/release-age-edit.ts +430 -0
  280. package/src/root-entries.ts +81 -0
  281. package/src/setup-template-scripts.ts +571 -0
  282. package/src/setup-templates.ts +471 -0
  283. package/src/skills.ts +161 -18
  284. package/src/status.ts +557 -0
  285. package/src/types.ts +148 -13
@@ -0,0 +1,520 @@
1
+ // AUTO-GENERATED by this package's build-time packer (issue #1475).
2
+ // Do not edit by hand -- edits are overwritten on the next `npm run build`.
3
+ // A byte-identical copy of the contract checker in @clossys/advisor (its
4
+ // source file is not shipped in this package), the one implementation, for a
5
+ // package that has no runtime dependency on @clossys/advisor.
6
+
7
+ /**
8
+ * A minimal JSON Schema (draft-07) checker for this repository's shared
9
+ * contracts (issue #1475): the plan record and the engagement brief, which
10
+ * two packages validate against without either depending on the other.
11
+ *
12
+ * It implements exactly the keywords those contracts use, and throws on any
13
+ * other keyword, or keyword form, it meets instead of half-checking it.
14
+ * `assertImplementedContract()` walks a whole contract, so a test can prove
15
+ * every subschema is implemented, visited by a value or not. Property
16
+ * lookups use Object.hasOwn, so an inherited name (toString, constructor,
17
+ * __proto__) is never mistaken for a declared or present property.
18
+ *
19
+ * Messages, paths and errors never echo text from the document under test
20
+ * -- neither a value nor a key. A brief can carry founder text, a refusal
21
+ * can end up in a log, and these messages are read by agents: a hostile
22
+ * document can put prompt-injection text in a key name as easily as in a
23
+ * value. A path names only fields the contract declares (schema text, not
24
+ * document text) and array indices. A field the contract does not declare,
25
+ * or a repeated key, is reported at the object that holds it, by the key's
26
+ * 1-based position in that object ("key 3 of this object"), never by name.
27
+ *
28
+ * Every string, and every object key, must be well-formed Unicode: a lone
29
+ * surrogate is refused whatever the contract says, because it has no UTF-8
30
+ * encoding and so no canonical form. A document this checker accepts can
31
+ * always be digested.
32
+ *
33
+ * `readContractDocument()` is the one way to turn a plan or brief FILE into
34
+ * a value: it refuses bytes that are not valid UTF-8 and any object that
35
+ * repeats a key, at any depth. `JSON.parse` alone would silently keep the
36
+ * last of two duplicate keys, so a reviewer reading the file could see a
37
+ * value that is not the one validated and digested.
38
+ *
39
+ * This file imports nothing. It is the one implementation: a package that
40
+ * cannot import it carries a generated, byte-identical copy.
41
+ */
42
+
43
+ export type ContractSchema = Readonly<Record<string, unknown>>;
44
+
45
+ /**
46
+ * One place a value breaks its contract. `path` is `""` for the document
47
+ * itself, else like `blockers[0].nextAction.who`: only names the contract
48
+ * declares, and array indices. Neither `path` nor `message` ever carries
49
+ * document text.
50
+ */
51
+ export interface ContractViolation {
52
+ readonly path: string;
53
+ readonly message: string;
54
+ }
55
+
56
+ /** Resolves a file-level `$ref` (for example `engagement-context.json`) to that contract. Throws when it cannot. */
57
+ export type ContractLoader = (name: string) => ContractSchema;
58
+
59
+ const ANNOTATIONS = new Set(["$schema", "$id", "title", "description"]);
60
+ const IMPLEMENTED = new Set([
61
+ "$ref", "type", "const", "enum", "required", "properties", "additionalProperties",
62
+ "items", "minItems", "maxItems", "contains", "minLength", "pattern", "format", "oneOf", "allOf", "not", "definitions",
63
+ ]);
64
+
65
+ /**
66
+ * The `format` values this checker asserts (JSON Schema leaves `format`
67
+ * optional to assert; these contracts require it). Both are ISO 8601 as
68
+ * RFC 3339 profiles it, checked field by field rather than by shape alone:
69
+ *
70
+ * - `date`: `YYYY-MM-DD`, with month 01-12 and a day that exists in that
71
+ * month of that year (leap years included, so `2028-02-29` is a date and
72
+ * `2026-02-29` is not).
73
+ * - `date-time`: a `date`, then `T`, then `hh:mm` with optional `:ss` and
74
+ * fraction, then `Z` or `+hh:mm` / `-hh:mm`; hours 00-23, minutes 00-59,
75
+ * seconds 00-59 (no leap second), offset hours 00-23 and offset minutes
76
+ * 00-59.
77
+ *
78
+ * Either must also give a finite `Date.parse`, so a time that validates can
79
+ * always be ordered.
80
+ */
81
+ const FORMATS = new Set(["date", "date-time"]);
82
+ const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-](\d{2}):(\d{2})))?$/;
83
+
84
+ function inRange(text: string | undefined, low: number, high: number): boolean {
85
+ if (text === undefined) return true;
86
+ const value = Number(text);
87
+ return value >= low && value <= high;
88
+ }
89
+
90
+ function daysInMonth(year: number, month: number): number {
91
+ if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
92
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
93
+ }
94
+
95
+ function matchesFormat(format: string, value: string): boolean {
96
+ const match = DATE_TIME.exec(value);
97
+ if (match === null) return false;
98
+ const [, year, month, day, hour, minute, second, offsetHour, offsetMinute] = match;
99
+ const isDateTime = hour !== undefined;
100
+ if (isDateTime !== (format === "date-time")) return false;
101
+ if (!inRange(month, 1, 12)) return false;
102
+ if (!inRange(day, 1, daysInMonth(Number(year), Number(month)))) return false;
103
+ if (!inRange(hour, 0, 23) || !inRange(minute, 0, 59) || !inRange(second, 0, 59)) return false;
104
+ if (!inRange(offsetHour, 0, 23) || !inRange(offsetMinute, 0, 59)) return false;
105
+ return Number.isFinite(Date.parse(value));
106
+ }
107
+
108
+ /** Throws on any keyword, or keyword form, this checker does not implement -- for this node only. */
109
+ function assertImplementedNode(schema: ContractSchema, at: string): void {
110
+ for (const key of Object.keys(schema)) {
111
+ if (!ANNOTATIONS.has(key) && !IMPLEMENTED.has(key)) throw new Error(`contract checker does not implement "${key}" (at ${at})`);
112
+ }
113
+ if (Object.hasOwn(schema, "type") && typeof schema.type !== "string") throw new Error(`contract checker implements only a string "type" (at ${at})`);
114
+ if (Object.hasOwn(schema, "additionalProperties") && typeof schema.additionalProperties !== "boolean") {
115
+ throw new Error(`contract checker implements only a boolean "additionalProperties" (at ${at})`);
116
+ }
117
+ if (Object.hasOwn(schema, "items") && Array.isArray(schema.items)) throw new Error(`contract checker does not implement tuple "items" (at ${at})`);
118
+ if (Object.hasOwn(schema, "format") && !FORMATS.has(schema.format as string)) {
119
+ throw new Error(`contract checker does not implement format ${JSON.stringify(schema.format)} (at ${at})`);
120
+ }
121
+ }
122
+
123
+ /** Walks every subschema of `schema`, visited by a value or not, and throws on the first keyword this checker does not implement. */
124
+ export function assertImplementedContract(schema: ContractSchema, at = "#"): void {
125
+ assertImplementedNode(schema, at);
126
+ for (const key of ["properties", "definitions"]) {
127
+ for (const [name, child] of Object.entries((schema[key] ?? {}) as Record<string, ContractSchema>)) assertImplementedContract(child, `${at}/${key}/${name}`);
128
+ }
129
+ for (const key of ["items", "contains", "not"]) if (Object.hasOwn(schema, key)) assertImplementedContract(schema[key] as ContractSchema, `${at}/${key}`);
130
+ for (const key of ["oneOf", "allOf"]) {
131
+ (Array.isArray(schema[key]) ? (schema[key] as ContractSchema[]) : []).forEach((child, index) => assertImplementedContract(child, `${at}/${key}/${index}`));
132
+ }
133
+ }
134
+
135
+ /** Whether an array has a hole: an index below its length with no own element. JSON never produces one. */
136
+ function hasHoles(value: readonly unknown[]): boolean {
137
+ for (let index = 0; index < value.length; index += 1) if (!Object.hasOwn(value, index)) return true;
138
+ return false;
139
+ }
140
+
141
+ /**
142
+ * The JSON type of a value. An array with holes (`[1, , 3]`, or one whose
143
+ * length was set past its last element) is "sparse array", not "array": code
144
+ * that walks it with forEach skips the holes while an indexed loop reads
145
+ * undefined, so two readers could judge it differently. No JSON text
146
+ * produces one, so a document read from a file is never affected.
147
+ */
148
+ function typeOf(value: unknown): string {
149
+ if (value === null) return "null";
150
+ if (Array.isArray(value)) return hasHoles(value) ? "sparse array" : "array";
151
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
152
+ return typeof value;
153
+ }
154
+
155
+ /** With the `u` flag a surrogate pair is one code point, so this matches only a lone surrogate. */
156
+ const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
157
+ const NOT_WELL_FORMED = "must be well-formed Unicode, and contains a lone surrogate";
158
+
159
+ /**
160
+ * The path of a field the contract declares: `path.name`, or `path["name"]`
161
+ * for a name that is not identifier-like. `name` is always the contract's
162
+ * own text (a `properties` or `required` entry), never a key read only
163
+ * from the document: an undeclared key never reaches this function.
164
+ */
165
+ function declaredPath(path: string, name: string): string {
166
+ if (!/^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(name)) return `${path}[${JSON.stringify(name)}]`;
167
+ return path === "" ? name : `${path}.${name}`;
168
+ }
169
+
170
+ /**
171
+ * Each object's keys in the order the file wrote them, for a value
172
+ * `readContractDocument()` returned. A JavaScript object enumerates
173
+ * array-index keys ("0", "7") first, whatever order they were written in,
174
+ * so without this a key ordinal could differ from the file's.
175
+ */
176
+ const WRITTEN_KEY_ORDER = new WeakMap<object, readonly string[]>();
177
+
178
+ /**
179
+ * `record`'s keys in the order the checker visits them, which is also the
180
+ * order their 1-based positions count in: as the file wrote them when
181
+ * `record` came from `readContractDocument()` and still has exactly the keys
182
+ * it was read with, else the object's own key order (JavaScript's, which
183
+ * lists array-index keys such as "7" first). Linear in the number of keys.
184
+ */
185
+ function keyOrder(record: object): readonly string[] {
186
+ const own = Object.keys(record);
187
+ const written = WRITTEN_KEY_ORDER.get(record);
188
+ if (written !== undefined && written.length === own.length) {
189
+ const writtenSet = new Set(written);
190
+ if (own.every((key) => writtenSet.has(key))) return written;
191
+ }
192
+ return own;
193
+ }
194
+
195
+ const TYPE_NOUNS: Readonly<Record<string, string>> = {
196
+ object: "an object", array: "an array", string: "a string", number: "a number", integer: "an integer", boolean: "a boolean", null: "null",
197
+ };
198
+
199
+ /**
200
+ * What a value must be, for a type mismatch. A `title` that is already a
201
+ * description ("a string with ...") is used as it is; one that names a
202
+ * document ("Advisor plan") is added after the type: "an object (the
203
+ * Advisor plan)".
204
+ */
205
+ function describeType(type: string, title: unknown): string {
206
+ if (typeof title !== "string") return TYPE_NOUNS[type] ?? `of type ${type}`;
207
+ if (/^(a|an) /.test(title)) return title;
208
+ return `${TYPE_NOUNS[type] ?? `of type ${type}`} (the ${title})`;
209
+ }
210
+
211
+ interface Scope {
212
+ readonly root: ContractSchema;
213
+ readonly load: ContractLoader;
214
+ }
215
+
216
+ function resolveRef(ref: string, scope: Scope, at: string): { schema: ContractSchema; scope: Scope } {
217
+ const [file = "", pointer = ""] = ref.split("#");
218
+ const docRoot = file === "" ? scope.root : scope.load(file);
219
+ let target: unknown = docRoot;
220
+ for (const segment of pointer.split("/").filter(Boolean)) {
221
+ if (typeof target !== "object" || target === null || !Object.hasOwn(target, segment)) throw new Error(`unresolvable $ref ${ref} (at ${at})`);
222
+ target = (target as Record<string, unknown>)[segment];
223
+ }
224
+ return { schema: target as ContractSchema, scope: { root: docRoot, load: scope.load } };
225
+ }
226
+
227
+ /** Follows `$ref` chains to the node that actually carries keywords. */
228
+ function dereference(schema: ContractSchema, scope: Scope, at: string): { schema: ContractSchema; scope: Scope } {
229
+ let current = { schema, scope };
230
+ while (typeof current.schema.$ref === "string") current = resolveRef(current.schema.$ref, current.scope, at);
231
+ return current;
232
+ }
233
+
234
+ /** An undeclared field, by its 1-based position in its object -- never its name. */
235
+ function undeclaredFieldMessage(ordinal: number): string {
236
+ return `has a field the contract does not declare (key ${ordinal} of this object), and unknown fields are refused`;
237
+ }
238
+
239
+ function check(input: ContractSchema, value: unknown, inputScope: Scope, path: string): ContractViolation[] {
240
+ // Draft-07: $ref replaces every sibling keyword (siblings are annotations only).
241
+ const { schema, scope } = dereference(input, inputScope, path);
242
+ assertImplementedNode(schema, path);
243
+ const kind = typeOf(value);
244
+ const expected = schema.type;
245
+ if (typeof expected === "string" && !(expected === kind || (expected === "number" && kind === "integer"))) {
246
+ return [{ path, message: `must be ${describeType(expected, schema.title)}, got ${kind}` }];
247
+ }
248
+ // A schema node with no `type` still never accepts an array with holes.
249
+ if (kind === "sparse array") return [{ path, message: "must not be an array with holes" }];
250
+ const violations: ContractViolation[] = [];
251
+ if (Object.hasOwn(schema, "const") && JSON.stringify(schema.const) !== JSON.stringify(value)) violations.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
252
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => JSON.stringify(option) === JSON.stringify(value))) {
253
+ violations.push({ path, message: `must be one of: ${schema.enum.map((option) => JSON.stringify(option)).join(", ")}` });
254
+ }
255
+ if (typeof value === "string") {
256
+ if (LONE_SURROGATE.test(value)) violations.push({ path, message: NOT_WELL_FORMED });
257
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) violations.push({ path, message: `must be at least ${schema.minLength} character(s) long` });
258
+ if (typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value)) {
259
+ violations.push({ path, message: typeof schema.title === "string" ? `must be ${schema.title}` : `must match the pattern ${schema.pattern}` });
260
+ }
261
+ if (typeof schema.format === "string" && !matchesFormat(schema.format, value)) {
262
+ violations.push({ path, message: typeof schema.title === "string" ? `must be ${schema.title}` : `must be a valid ${schema.format}` });
263
+ }
264
+ }
265
+ if (kind === "array") {
266
+ const items = value as readonly unknown[];
267
+ if (typeof schema.minItems === "number" && items.length < schema.minItems) violations.push({ path, message: `must have at least ${schema.minItems} item(s)` });
268
+ if (typeof schema.maxItems === "number" && items.length > schema.maxItems) violations.push({ path, message: `must have at most ${schema.maxItems} item(s)` });
269
+ if (schema.items !== undefined) items.forEach((item, index) => violations.push(...check(schema.items as ContractSchema, item, scope, `${path}[${index}]`)));
270
+ if (schema.contains !== undefined && !items.some((item, index) => check(schema.contains as ContractSchema, item, scope, `${path}[${index}]`).length === 0)) {
271
+ violations.push({ path, message: `must contain an item matching ${JSON.stringify(schema.contains)}` });
272
+ }
273
+ }
274
+ if (kind === "object") {
275
+ const record = value as Record<string, unknown>;
276
+ const properties = (schema.properties ?? {}) as Record<string, ContractSchema>;
277
+ for (const name of (schema.required ?? []) as string[]) {
278
+ if (!Object.hasOwn(record, name)) violations.push({ path: declaredPath(path, name), message: "is required" });
279
+ }
280
+ keyOrder(record).forEach((name, index) => {
281
+ const child = record[name];
282
+ if (LONE_SURROGATE.test(name)) {
283
+ // The key itself is never echoed (see the header), and this one cannot even be written as UTF-8.
284
+ violations.push({ path, message: `has a key that ${NOT_WELL_FORMED}` });
285
+ return;
286
+ }
287
+ if (Object.hasOwn(properties, name)) violations.push(...check(properties[name] as ContractSchema, child, scope, declaredPath(path, name)));
288
+ // The key is document text, so it is never named: the violation is
289
+ // placed at the object that holds it, with the key's position there.
290
+ else if (schema.additionalProperties === false) violations.push({ path, message: undeclaredFieldMessage(index + 1) });
291
+ });
292
+ }
293
+ if (Array.isArray(schema.oneOf)) violations.push(...checkOneOf(schema.oneOf as ContractSchema[], value, scope, path, schema.title));
294
+ if (Array.isArray(schema.allOf)) for (const branch of schema.allOf as ContractSchema[]) violations.push(...check(branch, value, scope, path));
295
+ if (schema.not !== undefined && check(schema.not as ContractSchema, value, scope, path).length === 0) violations.push({ path, message: `must not match ${JSON.stringify(schema.not)}` });
296
+ return violations;
297
+ }
298
+
299
+ /**
300
+ * Exactly one branch must pass. When none does, the violations of the one
301
+ * closest branch are reported -- the branch whose `type` fits the value, or
302
+ * failing that the unique branch with the fewest violations -- so a refusal
303
+ * names the actual field at fault rather than only "no branch matched".
304
+ * When no single branch is closest, the node's own `title` says what the
305
+ * value must be, if it has one.
306
+ */
307
+ function checkOneOf(branches: readonly ContractSchema[], value: unknown, scope: Scope, path: string, title: unknown): ContractViolation[] {
308
+ const results = branches.map((branch) => check(branch, value, scope, path));
309
+ const passing = results.filter((result) => result.length === 0).length;
310
+ if (passing === 1) return [];
311
+ if (passing > 1) return [{ path, message: `matches ${passing} of the ${branches.length} allowed forms, and must match exactly one` }];
312
+ const kind = typeOf(value);
313
+ const typed = branches
314
+ .map((branch, index) => ({ index, type: dereference(branch, scope, path).schema.type }))
315
+ .filter((entry) => entry.type === kind || (entry.type === "number" && kind === "integer"));
316
+ if (typed.length === 1) return results[typed[0]!.index]!;
317
+ const fewest = Math.min(...results.map((result) => result.length));
318
+ const closest = results.filter((result) => result.length === fewest);
319
+ if (closest.length === 1) return closest[0]!;
320
+ if (typeof title === "string") return [{ path, message: `must be ${title}` }];
321
+ return [{ path, message: `does not match any of the ${branches.length} allowed forms` }];
322
+ }
323
+
324
+ /**
325
+ * Every violation of `value` against `contract`; empty when it conforms. A
326
+ * type mismatch stops there and returns alone, since every other keyword
327
+ * assumes the value is already the right type. Otherwise, on an object, any
328
+ * `const` or `enum` violation of the object itself comes before a missing
329
+ * required field, which comes before each present key's violations in the
330
+ * order the keys were written (see `keyOrder()`), then any from `oneOf`,
331
+ * `allOf` and `not`. Never throws for a bad value. Throws only when the
332
+ * contract itself uses a keyword this checker does not implement, or a
333
+ * `$ref` it cannot resolve -- a defect in the contract, not in the value.
334
+ */
335
+ export function validateAgainstContract(contract: ContractSchema, value: unknown, load: ContractLoader): ContractViolation[] {
336
+ return check(contract, value, { root: contract, load }, "");
337
+ }
338
+
339
+ /** `label.path message` (for example `plan.blockers[0].capabilityId is required`), or `label message` for the document itself. */
340
+ export function formatContractViolation(label: string, violation: ContractViolation): string {
341
+ const where = violation.path === "" ? label : violation.path.startsWith("[") ? `${label}${violation.path}` : `${label}.${violation.path}`;
342
+ return `${where} ${violation.message}`;
343
+ }
344
+
345
+ // ignoreBOM keeps a leading byte order mark in the text, so it is refused below rather than silently stripped.
346
+ const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
347
+ const JSON_WHITESPACE = " \t\n\r";
348
+ const JSON_NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
349
+ const JSON_ESCAPE = /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/y;
350
+
351
+ /**
352
+ * Why `readContractDocument()` refused a file, as data a caller can act on
353
+ * without parsing the message: `reason` says which rule failed, and
354
+ * `position` (never file text) is set only for a syntax error or a leading
355
+ * byte order mark. The message carries no file text either: a repeated key
356
+ * is named by its 1-based position in its object, and that object as the
357
+ * top-level object or by its position, never by any key.
358
+ *
359
+ * A position, here and in every message, is a 0-based index into the
360
+ * decoded text in UTF-16 code units: JavaScript's string index. It equals
361
+ * the byte offset only for ASCII text; a character outside the Basic
362
+ * Multilingual Plane (an emoji, say) counts as two.
363
+ */
364
+ export class ContractDocumentError extends Error {
365
+ constructor(
366
+ message: string,
367
+ readonly reason: "encoding" | "syntax" | "repeated-key",
368
+ readonly position?: number,
369
+ ) {
370
+ super(message);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Checks that `text` is exactly one JSON value (RFC 8259 grammar) with no
376
+ * object that repeats a key, at any depth. Keys are compared after
377
+ * unescaping, so `"a"` and `"\u0061"` are the same key. Every refusal is
378
+ * by position only, never with a snippet of the text or a key, because a
379
+ * plan or brief can carry founder prose and a key can carry anything.
380
+ *
381
+ * Returns every object's keys in the order written, one list per object in
382
+ * document (pre-)order, for `recordWrittenKeyOrder()`.
383
+ */
384
+ function checkStrictJson(text: string): string[][] {
385
+ const keyOrders: string[][] = [];
386
+ let index = 0;
387
+ const syntaxError = (): never => {
388
+ throw new ContractDocumentError(`is not valid JSON at position ${index}`, "syntax", index);
389
+ };
390
+ const skipWhitespace = () => {
391
+ while (index < text.length && JSON_WHITESPACE.includes(text[index]!)) index += 1;
392
+ };
393
+ const expect = (character: string) => {
394
+ if (text[index] !== character) syntaxError();
395
+ index += 1;
396
+ };
397
+ const readString = (): string => {
398
+ const start = index;
399
+ expect('"');
400
+ for (;;) {
401
+ if (index >= text.length) syntaxError();
402
+ const unit = text.charCodeAt(index);
403
+ if (unit === 0x22) break;
404
+ if (unit < 0x20) syntaxError();
405
+ if (unit === 0x5c) {
406
+ JSON_ESCAPE.lastIndex = index;
407
+ if (!JSON_ESCAPE.test(text)) syntaxError();
408
+ index = JSON_ESCAPE.lastIndex;
409
+ } else {
410
+ index += 1;
411
+ }
412
+ }
413
+ index += 1;
414
+ return JSON.parse(text.slice(start, index)) as string;
415
+ };
416
+ const scan = (topLevel: boolean): void => {
417
+ skipWhitespace();
418
+ const opening = text[index];
419
+ if (opening === "{" || opening === "[") {
420
+ const closing = opening === "{" ? "}" : "]";
421
+ const where = topLevel ? "the top-level object" : `the object at position ${index}`;
422
+ const keys: string[] = [];
423
+ if (opening === "{") keyOrders.push(keys);
424
+ const seen = new Set<string>();
425
+ index += 1;
426
+ skipWhitespace();
427
+ if (text[index] === closing) {
428
+ index += 1;
429
+ return;
430
+ }
431
+ for (;;) {
432
+ if (opening === "{") {
433
+ skipWhitespace();
434
+ const key = readString();
435
+ keys.push(key);
436
+ if (seen.has(key)) {
437
+ // Never the key itself: it is document text, and may be written to be read as an instruction.
438
+ throw new ContractDocumentError(`repeats a key (key ${keys.length} of ${where}); every key may appear once`, "repeated-key");
439
+ }
440
+ seen.add(key);
441
+ skipWhitespace();
442
+ expect(":");
443
+ }
444
+ scan(false);
445
+ skipWhitespace();
446
+ if (text[index] === ",") {
447
+ index += 1;
448
+ continue;
449
+ }
450
+ expect(closing);
451
+ return;
452
+ }
453
+ }
454
+ if (opening === '"') {
455
+ readString();
456
+ return;
457
+ }
458
+ for (const literal of ["true", "false", "null"]) {
459
+ if (text.startsWith(literal, index)) {
460
+ index += literal.length;
461
+ return;
462
+ }
463
+ }
464
+ JSON_NUMBER.lastIndex = index;
465
+ if (!JSON_NUMBER.test(text)) syntaxError();
466
+ index = JSON_NUMBER.lastIndex;
467
+ };
468
+ scan(true);
469
+ skipWhitespace();
470
+ if (index !== text.length) syntaxError();
471
+ return keyOrders;
472
+ }
473
+
474
+ /**
475
+ * Records, for each object in `value`, the key order `checkStrictJson()`
476
+ * read for it. `keyOrders` lists objects in document order, which is the
477
+ * order this walk meets them in: each object, then its members in the order
478
+ * written. Keys are unique (a repeat was already refused), so each names
479
+ * exactly one own property; `value[key]` reads an own `__proto__` too,
480
+ * because JSON.parse defines it as an own property that shadows the
481
+ * inherited accessor.
482
+ */
483
+ function recordWrittenKeyOrder(value: unknown, keyOrders: readonly string[][], next: { index: number }): void {
484
+ if (typeof value !== "object" || value === null) return;
485
+ if (Array.isArray(value)) {
486
+ for (const item of value) recordWrittenKeyOrder(item, keyOrders, next);
487
+ return;
488
+ }
489
+ const keys = keyOrders[next.index]!;
490
+ next.index += 1;
491
+ WRITTEN_KEY_ORDER.set(value, keys);
492
+ for (const key of keys) recordWrittenKeyOrder((value as Record<string, unknown>)[key], keyOrders, next);
493
+ }
494
+
495
+ /**
496
+ * Reads a plan or brief file's bytes as strict JSON: UTF-8 that decodes
497
+ * without error (never silently replaced with U+FFFD) and does not start
498
+ * with a byte order mark, exactly one JSON value, and no object that repeats a key at any depth -- the I-JSON rules
499
+ * RFC 8785 canonicalization assumes. Refuses bytes that break a rule with a
500
+ * ContractDocumentError whose message says which rule, by position only: a
501
+ * syntax error by its position (a UTF-16 code-unit index, see
502
+ * ContractDocumentError), a repeated key by its 1-based position in its
503
+ * object and that object's position (or "the top-level object") -- never
504
+ * any text from the file. It does not validate the value against a contract; call
505
+ * `validateAgainstContract()` next, which then numbers an undeclared field
506
+ * by its position as written in the file.
507
+ */
508
+ export function readContractDocument(bytes: Uint8Array): unknown {
509
+ let text: string;
510
+ try {
511
+ text = STRICT_UTF8.decode(bytes);
512
+ } catch {
513
+ throw new ContractDocumentError("is not valid UTF-8", "encoding");
514
+ }
515
+ if (text.charCodeAt(0) === 0xfeff) throw new ContractDocumentError("is not valid JSON at position 0: it starts with a byte order mark, which strict JSON refuses", "syntax", 0);
516
+ const keyOrders = checkStrictJson(text);
517
+ const value = JSON.parse(text) as unknown;
518
+ recordWrittenKeyOrder(value, keyOrders, { index: 0 });
519
+ return value;
520
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by this package's build-time packer (issue #1178).
2
+ // Do not edit by hand -- edits are overwritten on the next `npm run build`.
3
+ // Source of truth: the one file in this package's source repository that sets
4
+ // the publishing scope and registry (not shipped in this package).
5
+
6
+ /** The publishing scope and the registry packages are published to and read from. */
7
+ export const PACKAGE_SCOPE: { readonly scope: string; readonly registry: string } = {
8
+ "scope": "@clossys",
9
+ "registry": "https://registry.npmjs.org"
10
+ };