skill-family-engineering-kit 0.1.2 → 0.2.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/src/workspace.mjs CHANGED
@@ -101,7 +101,11 @@ export async function listTargetEntries(root) {
101
101
  }
102
102
  if (dirent.isFile()) {
103
103
  entries.push({ path: rel, kind: "file" });
104
+ continue;
104
105
  }
106
+ // Anything else (FIFO, socket, device, …) is a special entry: it is
107
+ // recorded so classification can fail closed on it; it is never opened.
108
+ entries.push({ path: rel, kind: "special" });
105
109
  }
106
110
  }
107
111
  await walk(root, "");
@@ -169,6 +173,61 @@ export function matchAnyGlob(patterns, relPath) {
169
173
  return Array.isArray(patterns) && patterns.some((pattern) => matchGlob(pattern, relPath));
170
174
  }
171
175
 
176
+ function gitignoreSegmentRegExp(segment) {
177
+ let expression = "";
178
+ for (const char of segment) {
179
+ if (char === "*") expression += "[^/]*";
180
+ else expression += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
181
+ }
182
+ return new RegExp(`^${expression}$`);
183
+ }
184
+
185
+ /**
186
+ * Gitignore-style lexical match for file-registry artifact patterns. The
187
+ * registry's artifacts class is mechanically identical to the .gitignore
188
+ * patterns (check-structure enforces the equality), so the closed-world
189
+ * classification interprets them with gitignore semantics:
190
+ * - a leading "/" anchors the pattern at the target root;
191
+ * - a "/" in the middle (after the trailing one is stripped) also anchors;
192
+ * - a trailing "/" marks a directory pattern: it matches everything under
193
+ * a directory with that name, never a regular file of that name;
194
+ * - "*" is a per-segment wildcard (never crosses "/");
195
+ * - unanchored patterns match at any depth.
196
+ * Purely lexical; never touches the filesystem.
197
+ */
198
+ export function matchGitignoreStylePattern(pattern, relPath) {
199
+ if (typeof pattern !== "string" || typeof relPath !== "string") return false;
200
+ let trimmed = pattern.trim();
201
+ if (trimmed === "" || trimmed.startsWith("#")) return false;
202
+ let rootAnchored = trimmed.startsWith("/");
203
+ if (rootAnchored) trimmed = trimmed.slice(1);
204
+ const directoryOnly = trimmed.endsWith("/");
205
+ if (directoryOnly) trimmed = trimmed.slice(0, -1);
206
+ if (trimmed.includes("/")) rootAnchored = true;
207
+ const patternSegments = trimmed.split("/").filter((segment) => segment !== "");
208
+ if (patternSegments.length === 0) return false;
209
+ const pathSegments = relPath.split("/").filter((segment) => segment !== "");
210
+ const matchesAt = (start) => {
211
+ if (start + patternSegments.length > pathSegments.length) return false;
212
+ for (let i = 0; i < patternSegments.length; i += 1) {
213
+ if (!gitignoreSegmentRegExp(patternSegments[i]).test(pathSegments[start + i])) return false;
214
+ }
215
+ const end = start + patternSegments.length;
216
+ // A directory pattern matches everything strictly under the directory;
217
+ // a file pattern matches exactly the named entry.
218
+ return directoryOnly ? end < pathSegments.length : end === pathSegments.length;
219
+ };
220
+ if (rootAnchored) return matchesAt(0);
221
+ for (let start = 0; start < pathSegments.length; start += 1) {
222
+ if (matchesAt(start)) return true;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ export function matchAnyGitignorePattern(patterns, relPath) {
228
+ return Array.isArray(patterns) && patterns.some((pattern) => matchGitignoreStylePattern(pattern, relPath));
229
+ }
230
+
172
231
  /** Normalizes a relative path to forward slashes without touching the fs. */
173
232
  export function normalizeRelPath(relPath) {
174
233
  return String(relPath).replaceAll("\\", "/").replace(/\/+/g, "/").replace(/^\.\//, "");
@@ -189,6 +248,8 @@ export async function loadTargetFacts(root) {
189
248
  managedLock: null,
190
249
  managedSet: new Set(),
191
250
  handwrittenPatterns: [...DEFAULT_HANDWRITTEN_PATTERNS],
251
+ artifactPatterns: [],
252
+ trackedToolLocks: [],
192
253
  hasOwnRegistry: false,
193
254
  };
194
255
 
@@ -196,16 +257,37 @@ export async function loadTargetFacts(root) {
196
257
  if (registry.ok && registry.value && typeof registry.value === "object") {
197
258
  facts.fileRegistry = registry.value;
198
259
  facts.hasOwnRegistry = true;
260
+ // Foundation-shape registry: one flat managed list.
199
261
  const managed = registry.value?.classes?.managed;
200
262
  if (Array.isArray(managed)) {
201
263
  for (const entry of managed) {
202
264
  if (typeof entry === "string") facts.managedSet.add(normalizeRelPath(entry));
203
265
  }
204
266
  }
267
+ // Generated-skeleton registry: the managed set is declared as two classes
268
+ // (projen-managed + kit-managed). Both shapes feed the same closed world.
269
+ for (const classKey of ["projenManaged", "kitManaged"]) {
270
+ const files = registry.value?.classes?.[classKey]?.files;
271
+ if (Array.isArray(files)) {
272
+ for (const entry of files) {
273
+ if (typeof entry === "string") facts.managedSet.add(normalizeRelPath(entry));
274
+ }
275
+ }
276
+ }
205
277
  const patterns = registry.value?.classes?.handwritten?.entries;
206
278
  if (Array.isArray(patterns) && patterns.length > 0) {
207
279
  facts.handwrittenPatterns = patterns.filter((entry) => typeof entry === "string");
208
280
  }
281
+ const artifactPatterns = registry.value?.classes?.artifacts?.patterns;
282
+ if (Array.isArray(artifactPatterns)) {
283
+ facts.artifactPatterns = artifactPatterns.filter((entry) => typeof entry === "string");
284
+ }
285
+ const toolLocks = registry.value?.classes?.trackedToolLocks?.entries;
286
+ if (Array.isArray(toolLocks)) {
287
+ facts.trackedToolLocks = toolLocks
288
+ .filter((entry) => typeof entry === "string")
289
+ .map((entry) => normalizeRelPath(entry));
290
+ }
209
291
  }
210
292
 
211
293
  const manifest = await readOptionalJson(root, "skill-family.project-manifest.json");