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
package/src/errors.mjs ADDED
@@ -0,0 +1,144 @@
1
+ import { ContractsError, isRegisteredErrorCode } from "skill-family-contracts";
2
+
3
+ /**
4
+ * Engineering kit error policy.
5
+ *
6
+ * The kit never invents error codes: every rejection carries a code from the
7
+ * frozen contracts registry (skill-family-contracts owns the registry; adding
8
+ * a code there would be a contracts change outside this package's write set).
9
+ *
10
+ * Mapping onto the frozen SFC codes:
11
+ * - SFC2002 (UNKNOWN_OPERATION) — intake rejected a name outside the frozen
12
+ * four-command vocabulary.
13
+ * - SFC2003 (INVALID_PARAMS) — intake rejected option/parameter values
14
+ * (unknown flags, malformed ids, requests
15
+ * for mutation modes the kit does not have).
16
+ * - SFC2004 (EXECUTION_FAILED) — a well-formed command failed while
17
+ * executing; details.kind carries one stable
18
+ * KIT_ERROR_KINDS value (same pairing the
19
+ * harness uses for mechanism evidence).
20
+ * - SFC1001 (SCHEMA_VALIDATION_FAILED) — a contract document discovered in a
21
+ * target failed its registered schema (the
22
+ * check command reports these).
23
+ *
24
+ * HarnessError instances thrown by skill-family-harness-node are already
25
+ * coded (SFC2004 + their own stable kinds) and propagate unchanged; the kit
26
+ * never re-wraps or masks them.
27
+ */
28
+
29
+ /**
30
+ * Stable kit-level failure kinds. Each value appears as `details.kind` on a
31
+ * KitError or as the `kind` of a plan/check finding. The set is frozen for
32
+ * the v1 kit; values are strings so they serialize unchanged.
33
+ */
34
+ export const KIT_ERROR_KINDS = Object.freeze({
35
+ // scaffold
36
+ TARGET_NOT_DIRECTORY: "target-not-directory",
37
+ TARGET_NOT_EMPTY: "target-not-empty",
38
+ INVALID_ROOT: "invalid-root",
39
+ // projection
40
+ INVALID_MANIFEST: "invalid-manifest",
41
+ UNAUTHORIZED_PATH: "unauthorized-path",
42
+ HANDWRITTEN_OVERWRITE: "handwritten-overwrite",
43
+ SELF_PROJECTION: "self-projection",
44
+ CONFLICT_DRIFT: "conflict-drift",
45
+ TYPE_CONFLICT: "type-conflict",
46
+ SYMLINK_ON_PLANNED_PATH: "symlink-on-planned-path",
47
+ PROJECTION_WRITE_FAILED: "projection-write-failed",
48
+ // check / shared
49
+ CONTRACTS_MISSING: "contracts-missing",
50
+ CONTRACT_PARSE_FAILED: "contract-parse-failed",
51
+ MANAGED_FILE_MISSING: "managed-file-missing",
52
+ MANAGED_FILE_DRIFT: "managed-file-drift",
53
+ SYMLINK_AT_MANAGED_PATH: "symlink-at-managed-path",
54
+ CLOSURE_INPUT_MISSING: "closure-input-missing",
55
+ CONTRACTS_VERSION_MISMATCH: "contracts-version-mismatch",
56
+ README_MISSING: "readme-missing",
57
+ IDENTITY_MISMATCH: "identity-mismatch",
58
+ IDENTITY_RECORD_MISSING: "identity-record-missing",
59
+ GIT_NO_COMMITS: "git-no-commits",
60
+ GIT_DIRTY: "git-dirty",
61
+ MUTATION_MODE_REQUESTED: "mutation-mode-requested",
62
+ });
63
+
64
+ /** Error carrying one frozen SFC code plus a stable kit kind in details. */
65
+ export class KitError extends ContractsError {
66
+ constructor(code, message, details) {
67
+ if (!isRegisteredErrorCode(code)) {
68
+ throw new TypeError(
69
+ `KitError refuses unregistered error code: ${String(code)}`,
70
+ );
71
+ }
72
+ super(code, message, details);
73
+ this.name = "KitError";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Builds the canonical kit execution-failure error: SFC2004 with a stable
79
+ * details.kind. Extra structured evidence may be merged into details but can
80
+ * never override the kind.
81
+ */
82
+ export function kitError(kind, message, extraDetails) {
83
+ const values = Object.values(KIT_ERROR_KINDS);
84
+ if (!values.includes(kind)) {
85
+ throw new TypeError(`kitError: unknown kit error kind: ${String(kind)}`);
86
+ }
87
+ const details = { ...(extraDetails ?? {}), kind };
88
+ return new KitError("SFC2004", message, details);
89
+ }
90
+
91
+ /**
92
+ * Aggregate refusal error for projection refusals. The first refusal's kind
93
+ * becomes details.kind; it may be a kit kind OR a stable harness kind
94
+ * (path-traversal, symlink-escape, ...) that propagated unchanged — both
95
+ * sets are frozen and stable, and the SFC code stays registered.
96
+ */
97
+ export function refusalError(refusals, message, extraDetails) {
98
+ if (!Array.isArray(refusals) || refusals.length === 0) {
99
+ throw new TypeError("refusalError: refusals must be a non-empty array");
100
+ }
101
+ const first = refusals[0];
102
+ const kind = first && typeof first.kind === "string" && first.kind.length > 0 ? first.kind : "refused";
103
+ return new KitError("SFC2004", message, { ...(extraDetails ?? {}), kind, refusals });
104
+ }
105
+
106
+ /** Intake rejection: a name outside the frozen four-command vocabulary. */
107
+ export function unknownCommandError(name) {
108
+ return new KitError(
109
+ "SFC2002",
110
+ `unknown command: ${String(name)} (the kit has exactly four top-level commands)`,
111
+ { kind: "unknown-command", command: String(name) },
112
+ );
113
+ }
114
+
115
+ /** Intake rejection: option/parameter values violate the command contract. */
116
+ export function invalidParamsError(message, extraDetails) {
117
+ return new KitError("SFC2003", message, {
118
+ ...(extraDetails ?? {}),
119
+ kind: "invalid-params",
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Flags that request a mutation mode the kit structurally does not have.
125
+ * They are refused at intake (never silently ignored) so a caller cannot
126
+ * believe an auto-fix or an adopt-in-place happened.
127
+ */
128
+ export const REFUSED_MUTATION_FLAGS = Object.freeze([
129
+ "--fix",
130
+ "--apply",
131
+ "--adopt",
132
+ "--write",
133
+ "--repair",
134
+ "--force",
135
+ "--overwrite",
136
+ ]);
137
+
138
+ export function mutationModeError(flag, command) {
139
+ return new KitError(
140
+ "SFC2003",
141
+ `refused: '${flag}' requests a mutation mode the '${command}' command does not have (the kit never auto-fixes)`,
142
+ { kind: KIT_ERROR_KINDS.MUTATION_MODE_REQUESTED, flag },
143
+ );
144
+ }
@@ -0,0 +1,230 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Read-only Git pre-state diagnosis.
7
+ *
8
+ * The kit never performs Git writes. Diagnosis uses two layers:
9
+ *
10
+ * 1. Filesystem facts (no process is spawned at all): does `.git` exist,
11
+ * and does HEAD resolve to a commit? This layer works without the git
12
+ * binary and can never have a side effect.
13
+ *
14
+ * 2. One optional read-only `git status` query, strictly confined to the
15
+ * frozen argument vector GIT_STATUS_ARGS. GIT_OPTIONAL_LOCKS=0 and
16
+ * --no-optional-locks keep git from refreshing the index, and
17
+ * core.fsmonitor is disabled so no fsmonitor state is written. The
18
+ * spawn is best-effort: any failure (no binary, timeout, unreadable
19
+ * repository) degrades to filesystem facts with cleanState "unknown".
20
+ *
21
+ * The result is data for plans and check reports; it is never used to
22
+ * decide whether a write happens (writes are bounded by manifests and
23
+ * containment alone).
24
+ */
25
+
26
+ /** The single frozen read-only argument vector the kit may spawn. */
27
+ export const GIT_STATUS_ARGS = Object.freeze([
28
+ "--no-optional-locks",
29
+ "-c",
30
+ "core.fsmonitor=false",
31
+ "status",
32
+ "--porcelain=2",
33
+ ]);
34
+
35
+ /** Frozen read-only vector listing the index (tracked) paths. */
36
+ export const GIT_LS_FILES_ARGS = Object.freeze([
37
+ "--no-optional-locks",
38
+ "-c",
39
+ "core.fsmonitor=false",
40
+ "ls-files",
41
+ "-z",
42
+ ]);
43
+
44
+ /**
45
+ * Frozen read-only vector asking git's own ignore machinery which of the
46
+ * NUL-separated stdin paths are ignored. Real ignore semantics:
47
+ * globs, directory rules, negations and nested .gitignore files are all
48
+ * decided by git itself, never by a lexical reimplementation.
49
+ *
50
+ * --no-index evaluates the ignore rules purely: without it git skips
51
+ * index-tracked paths entirely, which would hide the tracked-but-ignored
52
+ * hazard. Rule files (.gitignore at every level, $GIT_DIR/info/exclude,
53
+ * core.excludesFile) stay authoritative; only the index filter is removed.
54
+ */
55
+ export const GIT_CHECK_IGNORE_ARGS = Object.freeze([
56
+ "--no-optional-locks",
57
+ "-c",
58
+ "core.fsmonitor=false",
59
+ "check-ignore",
60
+ "-z",
61
+ "--no-index",
62
+ "--stdin",
63
+ ]);
64
+
65
+ /** Every git invocation the kit is allowed to make (read-only verbs only). */
66
+ export const GIT_READ_ONLY_ALLOWLIST = Object.freeze([
67
+ GIT_STATUS_ARGS,
68
+ GIT_LS_FILES_ARGS,
69
+ GIT_CHECK_IGNORE_ARGS,
70
+ ]);
71
+
72
+ const PROBE_TIMEOUT_MS = 10_000;
73
+
74
+ function gitSpawnEnv() {
75
+ return {
76
+ ...process.env,
77
+ GIT_OPTIONAL_LOCKS: "0",
78
+ GIT_TERMINAL_PROMPT: "0",
79
+ GIT_CONFIG_NOSYSTEM: "1",
80
+ };
81
+ }
82
+
83
+ async function readGitFile(root, relPath) {
84
+ try {
85
+ return await readFile(path.join(root, ".git", relPath), "utf8");
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Resolves whether HEAD points at an existing commit, using only files
93
+ * under `.git`. Returns true/false/null (undecidable).
94
+ */
95
+ async function headHasCommitFs(root) {
96
+ const head = await readGitFile(root, "HEAD");
97
+ if (head === null) return null;
98
+ const trimmed = head.trim();
99
+ if (/^[0-9a-f]{40}$/.test(trimmed)) return true; // detached HEAD at a commit
100
+ const match = /^ref:\s*(.+)$/.exec(trimmed);
101
+ if (!match) return null;
102
+ const ref = match[1].trim();
103
+ const loose = await readGitFile(root, ref);
104
+ if (loose !== null && loose.trim().length > 0) return true;
105
+ const packed = await readGitFile(root, "packed-refs");
106
+ if (packed !== null) {
107
+ for (const line of packed.split("\n")) {
108
+ if (line.startsWith("#") || line.startsWith("^")) continue;
109
+ const parts = line.trim().split(/\s+/);
110
+ if (parts.length >= 2 && parts[1] === ref) return true;
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Probes the Git pre-state of root. Never writes anywhere; the only
118
+ * possible process spawn is the frozen read-only status query.
119
+ *
120
+ * Returns:
121
+ * {
122
+ * repository: boolean, // `.git` is present
123
+ * headCommit: true|false|null, // does HEAD resolve to a commit
124
+ * cleanState: true|false|null, // null when undecidable (no spawn/fs only)
125
+ * probe: "git-status"|"fs-only", // which layer produced cleanState
126
+ * spawn: "ok"|"skipped"|"failed", // whether the read-only spawn happened
127
+ * }
128
+ */
129
+ export async function probeGitState(root, { allowSpawn = true } = {}) {
130
+ const result = {
131
+ repository: false,
132
+ headCommit: null,
133
+ cleanState: null,
134
+ probe: "fs-only",
135
+ spawn: "skipped",
136
+ };
137
+
138
+ const head = await readGitFile(root, "HEAD");
139
+ if (head === null) return result; // no .git/HEAD => not a usable repository
140
+ result.repository = true;
141
+ result.headCommit = await headHasCommitFs(root);
142
+
143
+ if (!allowSpawn) return result;
144
+
145
+ const child = spawnSync("git", [...GIT_STATUS_ARGS], {
146
+ cwd: root,
147
+ encoding: "utf8",
148
+ timeout: PROBE_TIMEOUT_MS,
149
+ env: gitSpawnEnv(),
150
+ });
151
+ if (child.error || child.status !== 0) {
152
+ result.spawn = "failed";
153
+ return result;
154
+ }
155
+ result.spawn = "ok";
156
+ result.probe = "git-status";
157
+ result.cleanState = child.stdout.trim().length === 0;
158
+ return result;
159
+ }
160
+
161
+ function splitNulList(buffer) {
162
+ return buffer
163
+ .toString("utf8")
164
+ .split("\0")
165
+ .filter((entry) => entry.length > 0);
166
+ }
167
+
168
+ /**
169
+ * Proves or refuses the tracked/ignore facts for candidate paths.
170
+ *
171
+ * "tracked" comes exclusively from the read-only git index (ls-files) and
172
+ * "ignored" exclusively from git's own ignore machinery (check-ignore):
173
+ * declarations from project manifests or managed locks are passed in as
174
+ * candidate paths only and can never impersonate tracked state.
175
+ *
176
+ * Never fabricates true/false: when spawning is not allowed, the target is
177
+ * not a repository, the git binary is unusable, or any frozen vector fails,
178
+ * the outcome is { status: "not-proven", reason } — callers must report
179
+ * unknown, not guess.
180
+ *
181
+ * Returns either:
182
+ * { status: "not-proven", reason: "spawn-not-allowed"|"no-repository"|
183
+ * "no-candidates"|"git-unavailable" }
184
+ * { status: "proven", tracked, ignored, trackedButIgnored, ignoredNotTracked }
185
+ * trackedButIgnored / ignoredNotTracked are sorted arrays of candidate paths.
186
+ */
187
+ export async function probeGitFacts(root, candidatePaths, { allowSpawn = true } = {}) {
188
+ const notProven = (reason) => ({ status: "not-proven", reason });
189
+ const candidates = [
190
+ ...new Set(
191
+ (candidatePaths ?? [])
192
+ .filter((entry) => typeof entry === "string")
193
+ .map((entry) => entry.replaceAll("\\", "/").replace(/\/+/g, "/").replace(/^\.\//, ""))
194
+ .filter((entry) => entry.length > 0 && !entry.startsWith(".git/")),
195
+ ),
196
+ ].sort();
197
+ if (!allowSpawn) return notProven("spawn-not-allowed");
198
+ const head = await readGitFile(root, "HEAD");
199
+ if (head === null) return notProven("no-repository");
200
+ if (candidates.length === 0) return notProven("no-candidates");
201
+
202
+ const ls = spawnSync("git", [...GIT_LS_FILES_ARGS], {
203
+ cwd: root,
204
+ encoding: "buffer",
205
+ timeout: PROBE_TIMEOUT_MS,
206
+ env: gitSpawnEnv(),
207
+ });
208
+ if (ls.error || ls.status !== 0) return notProven("git-unavailable");
209
+ const tracked = new Set(splitNulList(ls.stdout));
210
+
211
+ const check = spawnSync("git", [...GIT_CHECK_IGNORE_ARGS], {
212
+ cwd: root,
213
+ input: Buffer.from(candidates.join("\0") + "\0", "utf8"),
214
+ encoding: "buffer",
215
+ timeout: PROBE_TIMEOUT_MS,
216
+ env: gitSpawnEnv(),
217
+ });
218
+ // check-ignore exits 0 when at least one path is ignored, 1 when none is;
219
+ // anything else (128 = not a repository, binary failure) is never a fact.
220
+ if (check.error || check.status === null || check.status > 1) return notProven("git-unavailable");
221
+ const ignored = new Set(splitNulList(check.stdout));
222
+
223
+ return {
224
+ status: "proven",
225
+ tracked,
226
+ ignored,
227
+ trackedButIgnored: candidates.filter((entry) => tracked.has(entry) && ignored.has(entry)),
228
+ ignoredNotTracked: candidates.filter((entry) => !tracked.has(entry) && ignored.has(entry)),
229
+ };
230
+ }