skill-family-engineering-kit 0.1.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 (80) hide show
  1. package/CODE_OF_CONDUCT.md +131 -0
  2. package/CONTRIBUTING.md +69 -0
  3. package/LICENSE +201 -0
  4. package/README.md +74 -0
  5. package/SECURITY.md +34 -0
  6. package/data/licensing/registry.json +211 -0
  7. package/data/licensing/schema.json +207 -0
  8. package/docs/.nojekyll +0 -0
  9. package/docs/404.html +613 -0
  10. package/docs/architecture/index.html +904 -0
  11. package/docs/assets/images/favicon.png +0 -0
  12. package/docs/assets/javascripts/bundle.d7400e89.min.js +16 -0
  13. package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  14. package/docs/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  15. package/docs/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  16. package/docs/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  17. package/docs/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  18. package/docs/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  19. package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  20. package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  21. package/docs/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  22. package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  23. package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  24. package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  25. package/docs/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  26. package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  27. package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  28. package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  29. package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  30. package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  31. package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  32. package/docs/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  33. package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  34. package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  35. package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  36. package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  37. package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  38. package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  39. package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  40. package/docs/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  41. package/docs/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  42. package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  43. package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  44. package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  45. package/docs/assets/javascripts/lunr/tinyseg.js +206 -0
  46. package/docs/assets/javascripts/lunr/wordcut.js +6708 -0
  47. package/docs/assets/javascripts/workers/search.2c215733.min.js +42 -0
  48. package/docs/assets/stylesheets/main.ec1eaa64.min.css +1 -0
  49. package/docs/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  50. package/docs/git-lifecycle/index.html +976 -0
  51. package/docs/help/index.html +907 -0
  52. package/docs/index.html +837 -0
  53. package/docs/integration/audit/baseline/audit-codes.json +62 -0
  54. package/docs/integration/audit/failure-evidence/index.html +872 -0
  55. package/docs/integration/audit/independence/index.html +841 -0
  56. package/docs/integration/audit/index.html +826 -0
  57. package/docs/integration/audit/mutation-taxonomy/index.html +1022 -0
  58. package/docs/integration/audit/schemas/audit-evidence.schema.json +182 -0
  59. package/docs/integration/audit/version-compatibility/index.html +845 -0
  60. package/docs/migration/index.html +1213 -0
  61. package/docs/quickstart/index.html +842 -0
  62. package/docs/search/search_index.json +1 -0
  63. package/docs/setup/index.html +941 -0
  64. package/docs/sitemap.xml +51 -0
  65. package/package.json +42 -0
  66. package/src/adopt-plan.mjs +594 -0
  67. package/src/check.mjs +415 -0
  68. package/src/cli.mjs +230 -0
  69. package/src/errors.mjs +144 -0
  70. package/src/gitprobe.mjs +230 -0
  71. package/src/identity-check.mjs +563 -0
  72. package/src/index.mjs +233 -0
  73. package/src/license-texts/Apache-2.0.txt +201 -0
  74. package/src/license-texts/MIT.txt +21 -0
  75. package/src/licensing.mjs +299 -0
  76. package/src/migration.mjs +427 -0
  77. package/src/projection.mjs +397 -0
  78. package/src/scaffold.mjs +178 -0
  79. package/src/skeleton.mjs +886 -0
  80. package/src/workspace.mjs +236 -0
@@ -0,0 +1,299 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readFileContained } from "skill-family-harness-node";
5
+ import { validateDocument } from "skill-family-contracts";
6
+ import { invalidParamsError, kitError, KIT_ERROR_KINDS } from "./errors.mjs";
7
+
8
+ // Single authority for licensing policy.
9
+ //
10
+ // The Kit implements only generic parsing and rendering. Concrete policy
11
+ // lives exclusively in profile data under profiles/licensing/, and
12
+ // profiles/licensing/schema.json is executed at runtime against every
13
+ // registry load through the contracts validator. There is no second
14
+ // authority in code: no frozen per-profile rules, no hardcoded profile ids,
15
+ // authors, years, or commercial triggers.
16
+
17
+ const KEBAB_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
18
+
19
+ /** Registry file and schema file inside a licensing profiles root. */
20
+ export const LICENSING_REGISTRY_FILE = "registry.json";
21
+ export const LICENSING_SCHEMA_FILE = "schema.json";
22
+ const LICENSING_DIALECT = "2020-12";
23
+
24
+ /**
25
+ * The default licensing profiles root resolves to the data closure
26
+ * shipped INSIDE this package (data/licensing), generated from
27
+ * profiles/licensing at synth time. An isolated install therefore needs no
28
+ * monorepo-relative path; an explicit --profiles-root always wins.
29
+ */
30
+ export function bundledProfilesRoot() {
31
+ return fileURLToPath(new URL("../data/licensing/", import.meta.url));
32
+ }
33
+
34
+ const APACHE_2_TEXT = readFileSync(
35
+ new URL("./license-texts/Apache-2.0.txt", import.meta.url),
36
+ "utf8",
37
+ );
38
+ const MIT_TEMPLATE = readFileSync(
39
+ new URL("./license-texts/MIT.txt", import.meta.url),
40
+ "utf8",
41
+ );
42
+
43
+ function failure(message, details = {}) {
44
+ return kitError(KIT_ERROR_KINDS.INVALID_MANIFEST, message, details);
45
+ }
46
+
47
+ function exactKeys(value, expected, label) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
49
+ throw failure(`${label} must be an object`);
50
+ }
51
+ const actual = Object.keys(value).sort();
52
+ const wanted = [...expected].sort();
53
+ if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
54
+ throw failure(`${label} fields must be exactly: ${wanted.join(", ")}`, {
55
+ actual,
56
+ expected: wanted,
57
+ });
58
+ }
59
+ }
60
+
61
+ async function loadSchema(profilesRoot) {
62
+ try {
63
+ return JSON.parse(await readFileContained(profilesRoot, LICENSING_SCHEMA_FILE, { encoding: "utf8" }));
64
+ } catch (cause) {
65
+ throw kitError(
66
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
67
+ "licensing profile schema is missing or invalid JSON",
68
+ { schema: LICENSING_SCHEMA_FILE, cause: cause?.message },
69
+ );
70
+ }
71
+ }
72
+
73
+ function validateAgainstSchema(document, schema, label) {
74
+ const validation = validateDocument(document, { schema, dialect: LICENSING_DIALECT, policy: "strict" });
75
+ if (!validation.valid) {
76
+ throw failure(`${label} failed the licensing schema (${LICENSING_SCHEMA_FILE})`, {
77
+ observedCode: validation.errorCode,
78
+ errors: validation.errors.slice(0, 5),
79
+ });
80
+ }
81
+ return validation.data;
82
+ }
83
+
84
+ /**
85
+ * Generic semantic invariants that hold for EVERY licensing profile, applied
86
+ * after the schema gate. These are structural rules of the mechanism, not
87
+ * per-profile policy values (those exist only in the registry data).
88
+ */
89
+ export function validateLicensingProfile(profile) {
90
+ exactKeys(
91
+ profile,
92
+ ["schemaVersion", "kind", "profile", "licensing", "identity", "preservation", "commercial"],
93
+ "licensing profile",
94
+ );
95
+ if (profile.schemaVersion !== 1 || profile.kind !== "skill-family.licensing-profile") {
96
+ throw failure("licensing profile envelope is invalid");
97
+ }
98
+
99
+ exactKeys(profile.profile, ["id", "name", "variant"], "profile identity");
100
+ const { id, variant } = profile.profile;
101
+ const key = `${id}/${variant}`;
102
+
103
+ exactKeys(
104
+ profile.licensing,
105
+ ["spdx", "noticeRequired", "copyrightStrategy", "copyrightYear"],
106
+ `${key} licensing`,
107
+ );
108
+ // NOTICE is an Apache-2.0 mechanism requirement, independent of profile.
109
+ if (profile.licensing.noticeRequired !== (profile.licensing.spdx === "Apache-2.0")) {
110
+ throw failure(`${key} noticeRequired must be ${profile.licensing.spdx === "Apache-2.0"} for ${profile.licensing.spdx}`);
111
+ }
112
+
113
+ exactKeys(profile.identity, ["authors", "displayValue", "ownerSeparation"], `${key} identity`);
114
+ if (!Array.isArray(profile.identity.authors) || profile.identity.authors.length === 0) {
115
+ throw failure(`${key} must declare at least one author`);
116
+ }
117
+ const seenAuthorIds = new Set();
118
+ profile.identity.authors.forEach((author, index) => {
119
+ exactKeys(author, ["id", "displayName", "role"], `${key} authors[${index}]`);
120
+ if (seenAuthorIds.has(author.id)) {
121
+ throw failure(`${key} author ids must be unique (duplicate: ${author.id})`);
122
+ }
123
+ seenAuthorIds.add(author.id);
124
+ });
125
+ exactKeys(
126
+ profile.identity.ownerSeparation,
127
+ ["required", "deriveOwnerFromAuthor"],
128
+ `${key} owner separation`,
129
+ );
130
+
131
+ exactKeys(
132
+ profile.preservation,
133
+ ["notice", "thirdPartyLicenses", "historicalCopyright", "unresolvedHistoricalAction"],
134
+ `${key} preservation`,
135
+ );
136
+ exactKeys(profile.commercial, ["enabled", "triggerCondition"], `${key} commercial`);
137
+ return profile;
138
+ }
139
+
140
+ async function loadRegistry(profilesRoot) {
141
+ if (typeof profilesRoot !== "string" || !profilesRoot) {
142
+ throw invalidParamsError("profilesRoot must be a non-empty path string");
143
+ }
144
+ let registry;
145
+ try {
146
+ registry = JSON.parse(await readFileContained(profilesRoot, LICENSING_REGISTRY_FILE, { encoding: "utf8" }));
147
+ } catch (cause) {
148
+ throw kitError(
149
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
150
+ "licensing profile registry is missing or invalid JSON",
151
+ { registry: LICENSING_REGISTRY_FILE, cause: cause?.message },
152
+ );
153
+ }
154
+ const schema = await loadSchema(profilesRoot);
155
+ // schema.json is executed here at runtime; it is the mechanical
156
+ // authority on registry shape and allowed policy values.
157
+ const validated = validateAgainstSchema(registry, schema, "licensing profile registry");
158
+ for (const profile of validated.profiles) validateLicensingProfile(profile);
159
+ const actualKeys = validated.profiles.map((profile) => `${profile.profile.id}/${profile.profile.variant}`);
160
+ if (new Set(actualKeys).size !== actualKeys.length) {
161
+ throw failure("licensing profile registry contains duplicate profile variants", { keys: actualKeys });
162
+ }
163
+ return validated;
164
+ }
165
+
166
+ /**
167
+ * Loads one licensing profile by id (and optional variant) from a profiles
168
+ * root. When profileId is omitted, the FIRST variant declared in the
169
+ * registry is selected: the default is a data fact, never a hardcoded name.
170
+ * A profile with multiple variants still requires an explicit variant.
171
+ */
172
+ export async function loadLicensingProfile({ profilesRoot, profileId, variant } = {}) {
173
+ const registry = await loadRegistry(profilesRoot);
174
+ if (profileId === undefined || profileId === null || profileId === "") {
175
+ const [first] = registry.profiles;
176
+ return structuredClone(first);
177
+ }
178
+ if (typeof profileId !== "string" || !KEBAB_PATTERN.test(profileId)) {
179
+ throw invalidParamsError("profileId must be a kebab-case string");
180
+ }
181
+ const candidates = registry.profiles.filter((profile) => profile.profile.id === profileId);
182
+ if (candidates.length === 0) {
183
+ throw failure(`unknown licensing profile: ${profileId}`, {
184
+ available: registry.profiles.map((profile) => `${profile.profile.id}/${profile.profile.variant}`),
185
+ });
186
+ }
187
+ let resolvedVariant = variant;
188
+ if (resolvedVariant === undefined || resolvedVariant === null || resolvedVariant === "") {
189
+ if (candidates.length !== 1 || candidates[0].profile.variant !== "default") {
190
+ throw failure(`variant is required for licensing profile ${profileId}`, {
191
+ allowedVariants: candidates.map((profile) => profile.profile.variant),
192
+ });
193
+ }
194
+ resolvedVariant = "default";
195
+ }
196
+ const match = candidates.find((profile) => profile.profile.variant === resolvedVariant);
197
+ if (!match) {
198
+ throw failure(`unknown variant "${resolvedVariant}" for profile "${profileId}"`, {
199
+ allowedVariants: candidates.map((profile) => profile.profile.variant),
200
+ });
201
+ }
202
+ return structuredClone(match);
203
+ }
204
+
205
+ export async function listLicensingProfiles(profilesRoot) {
206
+ const registry = await loadRegistry(profilesRoot);
207
+ return registry.profiles.map((profile) => ({
208
+ id: profile.profile.id,
209
+ variant: profile.profile.variant,
210
+ spdx: profile.licensing.spdx,
211
+ authors: profile.identity.authors.map((author) => author.displayName),
212
+ }));
213
+ }
214
+
215
+ function copyrightHolder(profile) {
216
+ return profile.identity.authors.map((author) => author.displayName).join(" and ");
217
+ }
218
+
219
+ /**
220
+ * Renders the LICENSE bytes for a loaded profile. Apache-2.0 is the complete
221
+ * standard license text WITHOUT any project or foundation attribution; author
222
+ * and copyright statements belong to the per-profile generated NOTICE. MIT
223
+ * embeds the profile's own copyright line. Deterministic: same profile data
224
+ * yields identical bytes.
225
+ */
226
+ export function generateLicenseContent(profile) {
227
+ validateLicensingProfile(profile);
228
+ if (profile.licensing.spdx === "Apache-2.0") {
229
+ return APACHE_2_TEXT.endsWith("\n") ? APACHE_2_TEXT : `${APACHE_2_TEXT}\n`;
230
+ }
231
+ return MIT_TEMPLATE
232
+ .replaceAll("{{YEAR}}", String(profile.licensing.copyrightYear))
233
+ .replaceAll("{{COPYRIGHT_HOLDER}}", copyrightHolder(profile))
234
+ .replace(/\n?$/, "\n");
235
+ }
236
+
237
+ export function generateNoticeContent(profile, { projectName }) {
238
+ validateLicensingProfile(profile);
239
+ if (!profile.licensing.noticeRequired) return null;
240
+ if (typeof projectName !== "string" || !projectName.trim()) {
241
+ throw invalidParamsError("projectName is required to generate NOTICE");
242
+ }
243
+ return [
244
+ projectName,
245
+ "=".repeat(projectName.length),
246
+ "",
247
+ `Copyright ${profile.licensing.copyrightYear} ${copyrightHolder(profile)}`,
248
+ "",
249
+ "Licensed under the Apache License, Version 2.0.",
250
+ "",
251
+ "Existing NOTICE entries, third-party license notices, and historical",
252
+ "copyright statements must be preserved. Unresolved attribution is reported",
253
+ "for human review and is never deleted or reassigned automatically.",
254
+ "",
255
+ ].join("\n");
256
+ }
257
+
258
+ function normalizeProjections(projections) {
259
+ if (projections === undefined) return [];
260
+ if (!Array.isArray(projections)) {
261
+ throw invalidParamsError("identity projections must be an array");
262
+ }
263
+ return projections.map((projection, index) => {
264
+ exactKeys(projection, ["path", "kind"], `identity projections[${index}]`);
265
+ if (typeof projection.path !== "string"
266
+ || path.posix.isAbsolute(projection.path)
267
+ || projection.path.includes("\\")
268
+ || projection.path.split("/").includes("..")
269
+ || !projection.path.endsWith(".json")) {
270
+ throw invalidParamsError(`identity projection path must be a contained JSON path: ${projection.path}`);
271
+ }
272
+ if (!["plugin-manifest", "marketplace", "public-snapshot"].includes(projection.kind)) {
273
+ throw invalidParamsError(`unknown identity projection kind: ${projection.kind}`);
274
+ }
275
+ return { path: projection.path, kind: projection.kind };
276
+ });
277
+ }
278
+
279
+ export function generateIdentityRecord(profile, { projectId, projectName, projections } = {}) {
280
+ validateLicensingProfile(profile);
281
+ return {
282
+ schemaVersion: 1,
283
+ kind: "skill-family.identity-record",
284
+ project: { id: projectId, name: projectName },
285
+ licensing: {
286
+ spdx: profile.licensing.spdx,
287
+ profile: profile.profile.id,
288
+ variant: profile.profile.variant,
289
+ copyrightYear: profile.licensing.copyrightYear,
290
+ },
291
+ authors: structuredClone(profile.identity.authors),
292
+ displayValue: profile.identity.displayValue,
293
+ copyrightStrategy: profile.licensing.copyrightStrategy,
294
+ ownerSeparation: structuredClone(profile.identity.ownerSeparation),
295
+ preservation: structuredClone(profile.preservation),
296
+ commercial: structuredClone(profile.commercial),
297
+ projections: normalizeProjections(projections),
298
+ };
299
+ }