@tech-leads-club/harness-toolkit 0.5.1 → 0.6.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.
@@ -0,0 +1,67 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import * as TJS from "typescript-json-schema";
4
+
5
+ /**
6
+ * why: `PartialPolicy`, not `Policy` — a human-written config is always partial, and `Policy`'s every
7
+ * field being non-optional would mark a minimal config invalid in every editor.
8
+ */
9
+ const ROOT_TYPE = "PartialPolicy";
10
+ const SOURCE_FILE = "src/core/policy/policy.types.ts";
11
+
12
+ function compilerOptionsFrom(root: string): Record<string, unknown> {
13
+ const raw = JSON.parse(readFileSync(join(root, "tsconfig.json"), "utf8")) as {
14
+ compilerOptions: Record<string, unknown>;
15
+ };
16
+ return raw.compilerOptions;
17
+ }
18
+
19
+ /**
20
+ * invariant: `noExtraProps` is what makes an unknown key (the `format` class of bug) a schema
21
+ * violation instead of something the schema silently accepts.
22
+ */
23
+ export function generateConfigSchema(rawRoot: string): Record<string, unknown> {
24
+ // why: TypeScript prints an import() type query against its own canonicalised path — a `.` segment
25
+ // or backslashes in the caller's spelling would silently defeat redactBuildPath's string match below.
26
+ const root = resolve(rawRoot);
27
+ const program = TJS.getProgramFromFiles([join(root, SOURCE_FILE)], compilerOptionsFrom(root), root);
28
+ const schema = TJS.generateSchema(program, ROOT_TYPE, {
29
+ required: true,
30
+ noExtraProps: true,
31
+ strictNullChecks: true,
32
+ });
33
+ if (schema === null) {
34
+ throw new Error(`typescript-json-schema produced no schema for ${ROOT_TYPE} in ${SOURCE_FILE}`);
35
+ }
36
+ const properties = (schema as { properties?: Record<string, unknown> }).properties ?? {};
37
+ const withSchemaProp = {
38
+ ...schema,
39
+ properties: {
40
+ // why: a JSON Schema meta-key, not a PartialPolicy field — typescript-json-schema never emits
41
+ // it, and `noExtraProps` would otherwise make every real config with a `$schema` line invalid.
42
+ $schema: { type: "string" },
43
+ ...properties,
44
+ },
45
+ };
46
+ return redactBuildPath(withSchemaProp, root);
47
+ }
48
+
49
+ /**
50
+ * hazard: a field typed `Partial<Policy["grind"]>` (an indexed-access type, not its own named alias)
51
+ * has no clean name to give its `$ref`, so the generator falls back to printing the full structural
52
+ * type — including an `import("<absolute path>")` type query for every cross-file reference inside
53
+ * it. That absolute path is the machine that ran the build, encoded twice: once raw in `$ref` targets
54
+ * that are also definition keys, once URI-percent-encoded in the `$ref` string itself. Published
55
+ * as-is, it would leak the CI runner's (or a contributor's) filesystem layout into a public schema.
56
+ */
57
+ // why: TypeScript always prints module specifiers with forward slashes, regardless of host OS — on
58
+ // Windows `root` itself is backslash-separated, so the raw string alone never matches what got printed.
59
+ function redactBuildPath(schema: Record<string, unknown>, root: string): Record<string, unknown> {
60
+ const posixRoot = root.replace(/\\/g, "/");
61
+ const candidates = [root, posixRoot, encodeURIComponent(root), encodeURIComponent(posixRoot)];
62
+ let serialized = JSON.stringify(schema);
63
+ for (const candidate of candidates) {
64
+ serialized = serialized.split(candidate).join(".");
65
+ }
66
+ return JSON.parse(serialized) as Record<string, unknown>;
67
+ }
package/bin/tlc-build.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  * entrypoint, and the missing bundle only surfaces when a hook fires on somebody's machine.
17
17
  */
18
18
  import { spawnSync } from "node:child_process";
19
- import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
19
+ import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
20
20
  import { basename, dirname, join } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
22
 
@@ -115,3 +115,28 @@ for (const entry of readdirSync(dist, { withFileTypes: true })) {
115
115
  }
116
116
 
117
117
  console.log(`tlc-build: ok (${bundles} bundles)`);
118
+
119
+ // why: dynamic, and a missing module degrades rather than fails — typescript-json-schema is a
120
+ // devDependency, absent from a plain install, and the dist rebuild above is this script's real
121
+ // recovery contract (bin/tlc-exec.mjs's "dist missing, run tlc-build.mjs" message). Never committed
122
+ // (.gitignore) either way, so nothing here needs the freshness gate `dist/` no longer has.
123
+ let generateConfigSchema;
124
+ try {
125
+ ({ generateConfigSchema } = await import("./generate-schema.ts"));
126
+ } catch (error) {
127
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") {
128
+ throw error;
129
+ }
130
+ console.log("tlc-build: schema.json skipped — typescript-json-schema is not installed here");
131
+ }
132
+
133
+ if (generateConfigSchema) {
134
+ try {
135
+ const schema = generateConfigSchema(root);
136
+ writeFileSync(join(root, "schema.json"), `${JSON.stringify(schema, null, 2)}\n`);
137
+ console.log("tlc-build: schema.json ok");
138
+ } catch (error) {
139
+ console.error(`tlc-build: schema.json generation failed — ${error instanceof Error ? error.message : error}`);
140
+ process.exit(1);
141
+ }
142
+ }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://unpkg.com/@tech-leads-club/harness-toolkit@0.5/schema.json",
2
3
  "version": 1,
3
4
  "mode": "solo",
4
5
  "subagents": {