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.
- package/CODE_OF_CONDUCT.md +131 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/SECURITY.md +34 -0
- package/data/licensing/registry.json +211 -0
- package/data/licensing/schema.json +207 -0
- package/docs/.nojekyll +0 -0
- package/docs/404.html +613 -0
- package/docs/architecture/index.html +904 -0
- package/docs/assets/images/favicon.png +0 -0
- package/docs/assets/javascripts/bundle.d7400e89.min.js +16 -0
- package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- package/docs/assets/javascripts/lunr/tinyseg.js +206 -0
- package/docs/assets/javascripts/lunr/wordcut.js +6708 -0
- package/docs/assets/javascripts/workers/search.2c215733.min.js +42 -0
- package/docs/assets/stylesheets/main.ec1eaa64.min.css +1 -0
- package/docs/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- package/docs/git-lifecycle/index.html +976 -0
- package/docs/help/index.html +907 -0
- package/docs/index.html +837 -0
- package/docs/integration/audit/baseline/audit-codes.json +62 -0
- package/docs/integration/audit/failure-evidence/index.html +872 -0
- package/docs/integration/audit/independence/index.html +841 -0
- package/docs/integration/audit/index.html +826 -0
- package/docs/integration/audit/mutation-taxonomy/index.html +1022 -0
- package/docs/integration/audit/schemas/audit-evidence.schema.json +182 -0
- package/docs/integration/audit/version-compatibility/index.html +845 -0
- package/docs/migration/index.html +1213 -0
- package/docs/quickstart/index.html +842 -0
- package/docs/search/search_index.json +1 -0
- package/docs/setup/index.html +941 -0
- package/docs/sitemap.xml +51 -0
- package/package.json +42 -0
- package/src/adopt-plan.mjs +594 -0
- package/src/check.mjs +415 -0
- package/src/cli.mjs +230 -0
- package/src/errors.mjs +144 -0
- package/src/gitprobe.mjs +230 -0
- package/src/identity-check.mjs +563 -0
- package/src/index.mjs +233 -0
- package/src/license-texts/Apache-2.0.txt +201 -0
- package/src/license-texts/MIT.txt +21 -0
- package/src/licensing.mjs +299 -0
- package/src/migration.mjs +427 -0
- package/src/projection.mjs +397 -0
- package/src/scaffold.mjs +178 -0
- package/src/skeleton.mjs +886 -0
- 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
|
+
}
|