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,236 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { KIT_ERROR_KINDS, kitError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read-only introspection of one target workspace.
|
|
7
|
+
*
|
|
8
|
+
* Everything here only reads. Absolute paths are resolved once at the edge
|
|
9
|
+
* (resolveTargetRoot); every deeper access goes through the harness
|
|
10
|
+
* containment layer with root-relative paths, so no introspection can ever
|
|
11
|
+
* be pointed outside the target.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Files the bounded walk never descends into (opaque tooling state). */
|
|
15
|
+
export const OPAQUE_DIRECTORIES = Object.freeze([".git", "node_modules"]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default handwritten patterns for targets that carry no file registry of
|
|
19
|
+
* their own. Mirrors the three-class file registration of the foundation
|
|
20
|
+
* workspace: what is obviously hand-maintained source material is never
|
|
21
|
+
* treated as a re-projectable managed artifact.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_HANDWRITTEN_PATTERNS = Object.freeze([
|
|
24
|
+
"**/*.md",
|
|
25
|
+
".projenrc.js",
|
|
26
|
+
"src/**",
|
|
27
|
+
"docs/**",
|
|
28
|
+
"test/**",
|
|
29
|
+
"tests/**",
|
|
30
|
+
"fixtures/**",
|
|
31
|
+
"profiles/**",
|
|
32
|
+
"scripts/**",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves and validates the target root directory.
|
|
37
|
+
* Returns the absolute path. Throws KitError (invalid-root /
|
|
38
|
+
* target-not-directory) when the root is unusable.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveTargetRoot(rootInput) {
|
|
41
|
+
if (typeof rootInput !== "string" || rootInput.length === 0) {
|
|
42
|
+
throw kitError(KIT_ERROR_KINDS.INVALID_ROOT, "target root must be a non-empty path string");
|
|
43
|
+
}
|
|
44
|
+
const root = path.resolve(rootInput);
|
|
45
|
+
let st;
|
|
46
|
+
try {
|
|
47
|
+
st = await lstat(root);
|
|
48
|
+
} catch {
|
|
49
|
+
throw kitError(
|
|
50
|
+
KIT_ERROR_KINDS.INVALID_ROOT,
|
|
51
|
+
"target root does not exist",
|
|
52
|
+
{ root: "<opaque>" },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (st.isSymbolicLink()) {
|
|
56
|
+
// A symlinked root is followed by every later fs call; accept it only
|
|
57
|
+
// when it resolves to a directory (checked via stat, which follows).
|
|
58
|
+
try {
|
|
59
|
+
const real = await stat(root);
|
|
60
|
+
if (!real.isDirectory()) {
|
|
61
|
+
throw kitError(KIT_ERROR_KINDS.TARGET_NOT_DIRECTORY, "target root resolves to a non-directory");
|
|
62
|
+
}
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
if (cause instanceof Error && cause.details) throw cause;
|
|
65
|
+
throw kitError(KIT_ERROR_KINDS.TARGET_NOT_DIRECTORY, "target root is a symbolic link that does not resolve");
|
|
66
|
+
}
|
|
67
|
+
return root;
|
|
68
|
+
}
|
|
69
|
+
if (!st.isDirectory()) {
|
|
70
|
+
throw kitError(KIT_ERROR_KINDS.TARGET_NOT_DIRECTORY, "target root exists but is not a directory");
|
|
71
|
+
}
|
|
72
|
+
return root;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively lists entries under root (relative POSIX paths, sorted).
|
|
77
|
+
* Read-only: uses readdir/lstat only, never follows directory symlinks, and
|
|
78
|
+
* skips opaque tooling directories. Symlinked files are reported with
|
|
79
|
+
* kind "symlink".
|
|
80
|
+
*/
|
|
81
|
+
export async function listTargetEntries(root) {
|
|
82
|
+
const entries = [];
|
|
83
|
+
async function walk(absDir, relBase) {
|
|
84
|
+
const dirents = await readdir(absDir, { withFileTypes: true });
|
|
85
|
+
for (const dirent of dirents) {
|
|
86
|
+
const rel = relBase === "" ? dirent.name : `${relBase}/${dirent.name}`;
|
|
87
|
+
if (dirent.isDirectory() && !dirent.isSymbolicLink()) {
|
|
88
|
+
if (OPAQUE_DIRECTORIES.includes(dirent.name)) {
|
|
89
|
+
// .git / node_modules are opaque at every depth: recorded as an
|
|
90
|
+
// entry, never descended into (matches the foundation tooling).
|
|
91
|
+
entries.push({ path: rel, kind: "directory-opaque" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
entries.push({ path: rel, kind: "directory" });
|
|
95
|
+
await walk(path.join(absDir, dirent.name), rel);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (dirent.isSymbolicLink()) {
|
|
99
|
+
entries.push({ path: rel, kind: "symlink" });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (dirent.isFile()) {
|
|
103
|
+
entries.push({ path: rel, kind: "file" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await walk(root, "");
|
|
108
|
+
entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
109
|
+
return entries;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Reads one contained file as utf8; missing files return null. */
|
|
113
|
+
export async function readOptionalFile(root, relPath) {
|
|
114
|
+
try {
|
|
115
|
+
return await readFile(path.join(root, relPath), "utf8");
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
if (cause && cause.code === "ENOENT") return null;
|
|
118
|
+
throw cause;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Parses one contained JSON file; returns { ok, value } or { ok:false, reason }. */
|
|
123
|
+
export async function readOptionalJson(root, relPath) {
|
|
124
|
+
const text = await readOptionalFile(root, relPath);
|
|
125
|
+
if (text === null) return { ok: false, reason: "missing" };
|
|
126
|
+
try {
|
|
127
|
+
return { ok: true, value: JSON.parse(text) };
|
|
128
|
+
} catch {
|
|
129
|
+
return { ok: false, reason: "parse-failed" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Minimal glob matcher for file-registry style patterns.
|
|
135
|
+
* Supported per-segment forms: literal, `*` (any chars except separator),
|
|
136
|
+
* and the multi-segment wildcard `**`. Matching is purely lexical.
|
|
137
|
+
*/
|
|
138
|
+
export function matchGlob(pattern, relPath) {
|
|
139
|
+
if (typeof pattern !== "string" || typeof relPath !== "string") return false;
|
|
140
|
+
const patternSegments = pattern.split("/").filter((segment) => segment !== "");
|
|
141
|
+
const pathSegments = relPath.split("/").filter((segment) => segment !== "");
|
|
142
|
+
return matchSegments(patternSegments, 0, pathSegments, 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function segmentToRegExp(segment) {
|
|
146
|
+
let expression = "";
|
|
147
|
+
for (const char of segment) {
|
|
148
|
+
if (char === "*") expression += "[^/]*";
|
|
149
|
+
else expression += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
150
|
+
}
|
|
151
|
+
return new RegExp(`^${expression}$`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function matchSegments(patternSegments, pi, pathSegments, si) {
|
|
155
|
+
if (pi === patternSegments.length) return si === pathSegments.length;
|
|
156
|
+
const segment = patternSegments[pi];
|
|
157
|
+
if (segment === "**") {
|
|
158
|
+
for (let take = 0; si + take <= pathSegments.length; take += 1) {
|
|
159
|
+
if (matchSegments(patternSegments, pi + 1, pathSegments, si + take)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (si === pathSegments.length) return false;
|
|
164
|
+
if (!segmentToRegExp(segment).test(pathSegments[si])) return false;
|
|
165
|
+
return matchSegments(patternSegments, pi + 1, pathSegments, si + 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function matchAnyGlob(patterns, relPath) {
|
|
169
|
+
return Array.isArray(patterns) && patterns.some((pattern) => matchGlob(pattern, relPath));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Normalizes a relative path to forward slashes without touching the fs. */
|
|
173
|
+
export function normalizeRelPath(relPath) {
|
|
174
|
+
return String(relPath).replaceAll("\\", "/").replace(/\/+/g, "/").replace(/^\.\//, "");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Loads the target's own authorization and classification facts:
|
|
179
|
+
* - `.foundation/file-registry.json` (managed list + handwritten patterns)
|
|
180
|
+
* - `skill-family.project-manifest.json` (managedFiles declaration)
|
|
181
|
+
* - `skill-family.managed-file-lock.json` (locked managed paths)
|
|
182
|
+
* All reads are optional and read-only; malformed documents simply do not
|
|
183
|
+
* contribute facts (the check command reports them separately).
|
|
184
|
+
*/
|
|
185
|
+
export async function loadTargetFacts(root) {
|
|
186
|
+
const facts = {
|
|
187
|
+
fileRegistry: null,
|
|
188
|
+
projectManifest: null,
|
|
189
|
+
managedLock: null,
|
|
190
|
+
managedSet: new Set(),
|
|
191
|
+
handwrittenPatterns: [...DEFAULT_HANDWRITTEN_PATTERNS],
|
|
192
|
+
hasOwnRegistry: false,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const registry = await readOptionalJson(root, path.join(".foundation", "file-registry.json"));
|
|
196
|
+
if (registry.ok && registry.value && typeof registry.value === "object") {
|
|
197
|
+
facts.fileRegistry = registry.value;
|
|
198
|
+
facts.hasOwnRegistry = true;
|
|
199
|
+
const managed = registry.value?.classes?.managed;
|
|
200
|
+
if (Array.isArray(managed)) {
|
|
201
|
+
for (const entry of managed) {
|
|
202
|
+
if (typeof entry === "string") facts.managedSet.add(normalizeRelPath(entry));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const patterns = registry.value?.classes?.handwritten?.entries;
|
|
206
|
+
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
207
|
+
facts.handwrittenPatterns = patterns.filter((entry) => typeof entry === "string");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const manifest = await readOptionalJson(root, "skill-family.project-manifest.json");
|
|
212
|
+
if (manifest.ok && manifest.value && typeof manifest.value === "object") {
|
|
213
|
+
facts.projectManifest = manifest.value;
|
|
214
|
+
const managedFiles = manifest.value.managedFiles;
|
|
215
|
+
if (Array.isArray(managedFiles)) {
|
|
216
|
+
for (const entry of managedFiles) {
|
|
217
|
+
if (typeof entry === "string") facts.managedSet.add(normalizeRelPath(entry));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lock = await readOptionalJson(root, "skill-family.managed-file-lock.json");
|
|
223
|
+
if (lock.ok && lock.value && typeof lock.value === "object") {
|
|
224
|
+
facts.managedLock = lock.value;
|
|
225
|
+
const entries = lock.value.entries;
|
|
226
|
+
if (Array.isArray(entries)) {
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
if (entry && typeof entry.path === "string") {
|
|
229
|
+
facts.managedSet.add(normalizeRelPath(entry.path));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return facts;
|
|
236
|
+
}
|