human-to-code 0.0.1 → 0.1.2
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/Readme.md +368 -38
- package/SECURITY.md +175 -11
- package/dist/adapters/general.d.ts +30 -0
- package/dist/adapters/general.d.ts.map +1 -0
- package/dist/adapters/general.js +77 -0
- package/dist/adapters/general.js.map +1 -0
- package/dist/adapters/node.d.ts +8 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +1071 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/python.d.ts +7 -0
- package/dist/adapters/python.d.ts.map +1 -0
- package/dist/adapters/python.js +540 -0
- package/dist/adapters/python.js.map +1 -0
- package/dist/adapters/rust.d.ts +7 -0
- package/dist/adapters/rust.d.ts.map +1 -0
- package/dist/adapters/rust.js +517 -0
- package/dist/adapters/rust.js.map +1 -0
- package/dist/analyzer-types.d.ts +161 -0
- package/dist/analyzer-types.d.ts.map +1 -0
- package/dist/analyzer-types.js +8 -0
- package/dist/analyzer-types.js.map +1 -0
- package/dist/analyzer-utils.d.ts +40 -0
- package/dist/analyzer-utils.d.ts.map +1 -0
- package/dist/analyzer-utils.js +591 -0
- package/dist/analyzer-utils.js.map +1 -0
- package/dist/analyzer.d.ts +21 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +111 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/certification.d.ts +102 -0
- package/dist/certification.d.ts.map +1 -0
- package/dist/certification.js +193 -0
- package/dist/certification.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +566 -0
- package/dist/cli.js.map +1 -0
- package/dist/compiler-skills.d.ts +25 -0
- package/dist/compiler-skills.d.ts.map +1 -0
- package/dist/compiler-skills.js +158 -0
- package/dist/compiler-skills.js.map +1 -0
- package/dist/compiler-tools.d.ts +29 -0
- package/dist/compiler-tools.d.ts.map +1 -0
- package/dist/compiler-tools.js +337 -0
- package/dist/compiler-tools.js.map +1 -0
- package/dist/config.d.ts +122 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +704 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +153 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +772 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +196 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +721 -0
- package/dist/contracts.js.map +1 -0
- package/dist/discovery.d.ts +34 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +312 -0
- package/dist/discovery.js.map +1 -0
- package/dist/documentation.d.ts +44 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +418 -0
- package/dist/documentation.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/patch.d.ts +71 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +394 -0
- package/dist/patch.js.map +1 -0
- package/dist/pinned-http.d.ts +15 -0
- package/dist/pinned-http.d.ts.map +1 -0
- package/dist/pinned-http.js +170 -0
- package/dist/pinned-http.js.map +1 -0
- package/dist/planner.d.ts +30 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +228 -0
- package/dist/planner.js.map +1 -0
- package/dist/provider.d.ts +168 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +512 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers.d.ts +41 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +954 -0
- package/dist/providers.js.map +1 -0
- package/dist/run-store.d.ts +24 -0
- package/dist/run-store.d.ts.map +1 -0
- package/dist/run-store.js +285 -0
- package/dist/run-store.js.map +1 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +228 -0
- package/dist/schemas.js.map +1 -0
- package/dist/secret-scan.d.ts +30 -0
- package/dist/secret-scan.d.ts.map +1 -0
- package/dist/secret-scan.js +122 -0
- package/dist/secret-scan.js.map +1 -0
- package/dist/snapshot.d.ts +33 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +136 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/support-matrix.d.ts +23 -0
- package/dist/support-matrix.d.ts.map +1 -0
- package/dist/support-matrix.js +199 -0
- package/dist/support-matrix.js.map +1 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +25 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +388 -0
- package/dist/validation.js.map +1 -0
- package/dist/workflow.d.ts +65 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +1636 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +36 -9
- package/.claude/settings.local.json +0 -12
- package/.github/workflows/ci.yml +0 -18
- package/CONTRIBUTING.md +0 -40
- package/src/cli.ts +0 -218
- package/src/config.ts +0 -186
- package/src/discovery.ts +0 -186
- package/src/index.ts +0 -22
- package/src/types.ts +0 -74
- package/test/config.test.ts +0 -97
- package/test/discovery.test.ts +0 -91
- package/tsconfig.json +0 -17
package/src/config.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured config loader.
|
|
3
|
-
*
|
|
4
|
-
* Config is deterministic JSON (`human-to-code.config.json`), never parsed by
|
|
5
|
-
* an LLM — see plan §1.7 / §2. A hand-rolled validator keeps the tool
|
|
6
|
-
* dependency-free and gives precise error messages.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFile } from "node:fs/promises";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import type {
|
|
13
|
-
Config,
|
|
14
|
-
ProviderConfig,
|
|
15
|
-
ProviderName,
|
|
16
|
-
TargetLanguage,
|
|
17
|
-
} from "./types.ts";
|
|
18
|
-
|
|
19
|
-
export const CONFIG_FILENAME = "human-to-code.config.json";
|
|
20
|
-
|
|
21
|
-
const LANGUAGES: readonly TargetLanguage[] = [
|
|
22
|
-
"typescript",
|
|
23
|
-
"javascript",
|
|
24
|
-
"python",
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const PROVIDERS: readonly ProviderName[] = [
|
|
28
|
-
"openai",
|
|
29
|
-
"anthropic",
|
|
30
|
-
"ollama",
|
|
31
|
-
"grok",
|
|
32
|
-
"gemini",
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Current, non-dated default model per provider. The Anthropic default is the
|
|
37
|
-
* current flagship — the README's `claude-3-5-sonnet-20241022` is retired.
|
|
38
|
-
*/
|
|
39
|
-
const DEFAULT_MODEL: Record<ProviderName, string> = {
|
|
40
|
-
anthropic: "claude-opus-4-8",
|
|
41
|
-
openai: "gpt-4o",
|
|
42
|
-
ollama: "llama3.2",
|
|
43
|
-
grok: "grok-4.1",
|
|
44
|
-
gemini: "gemini-2.5-pro",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** The current default model id for a provider. */
|
|
48
|
-
export function defaultModelFor(name: ProviderName): string {
|
|
49
|
-
return DEFAULT_MODEL[name];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const DEFAULT_CONFIG: Config = {
|
|
53
|
-
language: "typescript",
|
|
54
|
-
filesToIgnore: ["node_modules", ".git", "dist"],
|
|
55
|
-
allowNonHumanFiles: false,
|
|
56
|
-
provider: {
|
|
57
|
-
name: "anthropic",
|
|
58
|
-
model: DEFAULT_MODEL.anthropic,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Thrown for any malformed config; message is safe to show the user. */
|
|
63
|
-
export class ConfigError extends Error {}
|
|
64
|
-
|
|
65
|
-
function isObject(v: unknown): v is Record<string, unknown> {
|
|
66
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function validateProvider(raw: unknown): ProviderConfig {
|
|
70
|
-
if (!isObject(raw)) {
|
|
71
|
-
throw new ConfigError("`provider` must be an object.");
|
|
72
|
-
}
|
|
73
|
-
const name = raw.name;
|
|
74
|
-
if (typeof name !== "string" || !PROVIDERS.includes(name as ProviderName)) {
|
|
75
|
-
throw new ConfigError(
|
|
76
|
-
`\`provider.name\` must be one of: ${PROVIDERS.join(", ")}.`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
const providerName = name as ProviderName;
|
|
80
|
-
|
|
81
|
-
let model = raw.model;
|
|
82
|
-
if (model === undefined) {
|
|
83
|
-
model = DEFAULT_MODEL[providerName];
|
|
84
|
-
} else if (typeof model !== "string" || model.length === 0) {
|
|
85
|
-
throw new ConfigError("`provider.model` must be a non-empty string.");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const provider: ProviderConfig = { name: providerName, model: model as string };
|
|
89
|
-
|
|
90
|
-
if (raw.baseUrl !== undefined) {
|
|
91
|
-
if (typeof raw.baseUrl !== "string") {
|
|
92
|
-
throw new ConfigError("`provider.baseUrl` must be a string.");
|
|
93
|
-
}
|
|
94
|
-
// Enforce TLS — a bad base URL can serve poisoned strict (plan §3.7).
|
|
95
|
-
if (!raw.baseUrl.startsWith("https://")) {
|
|
96
|
-
throw new ConfigError("`provider.baseUrl` must use https://.");
|
|
97
|
-
}
|
|
98
|
-
provider.baseUrl = raw.baseUrl;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return provider;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Validate an already-parsed object into a fully-defaulted Config. */
|
|
105
|
-
export function validateConfig(raw: unknown): Config {
|
|
106
|
-
if (!isObject(raw)) {
|
|
107
|
-
throw new ConfigError("Config root must be a JSON object.");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const config: Config = {
|
|
111
|
-
...DEFAULT_CONFIG,
|
|
112
|
-
provider: { ...DEFAULT_CONFIG.provider },
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
if (raw.language !== undefined) {
|
|
116
|
-
if (
|
|
117
|
-
typeof raw.language !== "string" ||
|
|
118
|
-
!LANGUAGES.includes(raw.language as TargetLanguage)
|
|
119
|
-
) {
|
|
120
|
-
throw new ConfigError(
|
|
121
|
-
`\`language\` must be one of: ${LANGUAGES.join(", ")}.`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
config.language = raw.language as TargetLanguage;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (raw.filesToIgnore !== undefined) {
|
|
128
|
-
if (
|
|
129
|
-
!Array.isArray(raw.filesToIgnore) ||
|
|
130
|
-
!raw.filesToIgnore.every((f) => typeof f === "string")
|
|
131
|
-
) {
|
|
132
|
-
throw new ConfigError("`filesToIgnore` must be an array of strings.");
|
|
133
|
-
}
|
|
134
|
-
config.filesToIgnore = raw.filesToIgnore as string[];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (raw.allowNonHumanFiles !== undefined) {
|
|
138
|
-
if (typeof raw.allowNonHumanFiles !== "boolean") {
|
|
139
|
-
throw new ConfigError("`allowNonHumanFiles` must be a boolean.");
|
|
140
|
-
}
|
|
141
|
-
config.allowNonHumanFiles = raw.allowNonHumanFiles;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (raw.provider !== undefined) {
|
|
145
|
-
config.provider = validateProvider(raw.provider);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return config;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Load config from `<root>/human-to-code.config.json`. Returns the defaults
|
|
153
|
-
* (with a flag) when no config file exists so first-run works with zero setup.
|
|
154
|
-
*/
|
|
155
|
-
export async function loadConfig(
|
|
156
|
-
root: string,
|
|
157
|
-
): Promise<{ config: Config; fromFile: boolean }> {
|
|
158
|
-
const path = join(root, CONFIG_FILENAME);
|
|
159
|
-
if (!existsSync(path)) {
|
|
160
|
-
return {
|
|
161
|
-
config: { ...DEFAULT_CONFIG, provider: { ...DEFAULT_CONFIG.provider } },
|
|
162
|
-
fromFile: false,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let text: string;
|
|
167
|
-
try {
|
|
168
|
-
text = await readFile(path, "utf8");
|
|
169
|
-
} catch (err) {
|
|
170
|
-
throw new ConfigError(`Could not read ${CONFIG_FILENAME}: ${String(err)}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let parsed: unknown;
|
|
174
|
-
try {
|
|
175
|
-
parsed = JSON.parse(text);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
throw new ConfigError(`${CONFIG_FILENAME} is not valid JSON: ${String(err)}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return { config: validateConfig(parsed), fromFile: true };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Serialize the default config for `--init`. */
|
|
184
|
-
export function defaultConfigJson(): string {
|
|
185
|
-
return JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n";
|
|
186
|
-
}
|
package/src/discovery.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File discovery for the deterministic core.
|
|
3
|
-
*
|
|
4
|
-
* Responsibilities (plan §3.3–3.4):
|
|
5
|
-
* - Walk the project, classifying .human / .strict.human / config / secrets.
|
|
6
|
-
* - Apply a name-based ignore denylist + best-effort .gitignore.
|
|
7
|
-
* - Never follow symlinks (traversal safety).
|
|
8
|
-
* - Provide a git-tracked-secrets check so the CLI can refuse to run.
|
|
9
|
-
*
|
|
10
|
-
* Discovery is side-effect free; the CLI decides what to do with the result.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readdir } from "node:fs/promises";
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { relative, resolve, sep } from "node:path";
|
|
16
|
-
import { CONFIG_FILENAME } from "./config.ts";
|
|
17
|
-
import type { DiscoveryResult, SourceFile, SourceKind } from "./types.ts";
|
|
18
|
-
|
|
19
|
-
const SECRETS_FILENAME = "secrets.human";
|
|
20
|
-
const HUMAN_CONFIG_FILENAME = "config.human";
|
|
21
|
-
const STRICT_SUFFIX = ".strict.human";
|
|
22
|
-
const HUMAN_SUFFIX = ".human";
|
|
23
|
-
|
|
24
|
-
/** Names always skipped regardless of config. */
|
|
25
|
-
const ALWAYS_IGNORE = new Set([".git", "node_modules", CONFIG_FILENAME]);
|
|
26
|
-
|
|
27
|
-
function toPosix(p: string): string {
|
|
28
|
-
return sep === "/" ? p : p.split(sep).join("/");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function classify(basename: string): SourceKind | null {
|
|
32
|
-
if (basename === SECRETS_FILENAME) return "secrets";
|
|
33
|
-
if (basename === HUMAN_CONFIG_FILENAME) return "config";
|
|
34
|
-
if (basename.endsWith(STRICT_SUFFIX)) return "strict";
|
|
35
|
-
// A plain .human file that is not one of the special files above.
|
|
36
|
-
if (basename.endsWith(HUMAN_SUFFIX)) return "human";
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function strictSiblingOf(relPath: string): string {
|
|
41
|
-
// foo.human -> foo.strict.human
|
|
42
|
-
return relPath.slice(0, -HUMAN_SUFFIX.length) + STRICT_SUFFIX;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Recursively collect candidate files. Directories/files whose basename is in
|
|
47
|
-
* `ignore` are skipped. Symlinks are never followed.
|
|
48
|
-
*/
|
|
49
|
-
async function walk(
|
|
50
|
-
root: string,
|
|
51
|
-
dir: string,
|
|
52
|
-
ignore: Set<string>,
|
|
53
|
-
out: SourceFile[],
|
|
54
|
-
counters: { ignored: number },
|
|
55
|
-
): Promise<void> {
|
|
56
|
-
let entries;
|
|
57
|
-
try {
|
|
58
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
59
|
-
} catch {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
const abs = resolve(dir, entry.name);
|
|
65
|
-
|
|
66
|
-
if (entry.isSymbolicLink()) {
|
|
67
|
-
counters.ignored++;
|
|
68
|
-
continue; // traversal safety — never follow symlinks
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (ignore.has(entry.name)) {
|
|
72
|
-
counters.ignored++;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (entry.isDirectory()) {
|
|
77
|
-
await walk(root, abs, ignore, out, counters);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!entry.isFile()) continue;
|
|
82
|
-
|
|
83
|
-
const kind = classify(entry.name);
|
|
84
|
-
if (kind === null) {
|
|
85
|
-
// Not a file we track; not counted as "ignored" for reporting.
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const relPath = toPosix(relative(root, abs));
|
|
90
|
-
const file: SourceFile = { absPath: abs, relPath, kind };
|
|
91
|
-
if (kind === "human") {
|
|
92
|
-
file.strictSibling = strictSiblingOf(relPath);
|
|
93
|
-
}
|
|
94
|
-
out.push(file);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Return the subset of relPaths that git considers ignored (best effort). */
|
|
99
|
-
function gitIgnored(root: string, relPaths: string[]): Set<string> {
|
|
100
|
-
if (relPaths.length === 0) return new Set();
|
|
101
|
-
const inRepo = spawnSync(
|
|
102
|
-
"git",
|
|
103
|
-
["-C", root, "rev-parse", "--is-inside-work-tree"],
|
|
104
|
-
{ encoding: "utf8" },
|
|
105
|
-
);
|
|
106
|
-
if (inRepo.status !== 0 || inRepo.stdout.trim() !== "true") return new Set();
|
|
107
|
-
|
|
108
|
-
const res = spawnSync("git", ["-C", root, "check-ignore", "--stdin"], {
|
|
109
|
-
input: relPaths.join("\n"),
|
|
110
|
-
encoding: "utf8",
|
|
111
|
-
});
|
|
112
|
-
// Exit 0 => some ignored, 1 => none ignored, other => error (treat as none).
|
|
113
|
-
if (res.status !== 0 && res.status !== 1) return new Set();
|
|
114
|
-
return new Set(
|
|
115
|
-
res.stdout
|
|
116
|
-
.split("\n")
|
|
117
|
-
.map((l) => l.trim())
|
|
118
|
-
.filter(Boolean),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Discover and classify all source files under `rootInput`.
|
|
124
|
-
*
|
|
125
|
-
* @param rootInput directory to scan
|
|
126
|
-
* @param extraIgnore config.filesToIgnore (name-based denylist)
|
|
127
|
-
*/
|
|
128
|
-
export async function discover(
|
|
129
|
-
rootInput: string,
|
|
130
|
-
extraIgnore: string[] = [],
|
|
131
|
-
): Promise<DiscoveryResult> {
|
|
132
|
-
const root = resolve(rootInput);
|
|
133
|
-
const ignore = new Set<string>([...ALWAYS_IGNORE, ...extraIgnore]);
|
|
134
|
-
|
|
135
|
-
const found: SourceFile[] = [];
|
|
136
|
-
const counters = { ignored: 0 };
|
|
137
|
-
await walk(root, root, ignore, found, counters);
|
|
138
|
-
|
|
139
|
-
// Best-effort .gitignore filtering on top of the name denylist.
|
|
140
|
-
const ignored = gitIgnored(
|
|
141
|
-
root,
|
|
142
|
-
found.map((f) => f.relPath),
|
|
143
|
-
);
|
|
144
|
-
const kept = found.filter((f) => {
|
|
145
|
-
// Secrets are always surfaced (so we can enforce safety), even if ignored.
|
|
146
|
-
if (f.kind === "secrets") return true;
|
|
147
|
-
if (ignored.has(f.relPath)) {
|
|
148
|
-
counters.ignored++;
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result: DiscoveryResult = {
|
|
155
|
-
root,
|
|
156
|
-
human: kept.filter((f) => f.kind === "human"),
|
|
157
|
-
strict: kept.filter((f) => f.kind === "strict"),
|
|
158
|
-
ignoredCount: counters.ignored,
|
|
159
|
-
};
|
|
160
|
-
const secrets = kept.find((f) => f.kind === "secrets");
|
|
161
|
-
if (secrets) result.secrets = secrets;
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Refuse to run if `secrets.human` is tracked by git — a committed secrets file
|
|
167
|
-
* is the mistake we most want to prevent (plan §3.3). No-op outside a git repo.
|
|
168
|
-
* @returns an error message if unsafe, otherwise null.
|
|
169
|
-
*/
|
|
170
|
-
export function secretsTrackedError(secret: SourceFile): string | null {
|
|
171
|
-
if (secret.kind !== "secrets") return null;
|
|
172
|
-
const root = resolve(secret.absPath, "..");
|
|
173
|
-
const res = spawnSync(
|
|
174
|
-
"git",
|
|
175
|
-
["-C", root, "ls-files", "--error-unmatch", secret.relPath],
|
|
176
|
-
{ encoding: "utf8" },
|
|
177
|
-
);
|
|
178
|
-
if (res.status === 0) {
|
|
179
|
-
return (
|
|
180
|
-
`Refusing to run: ${secret.relPath} is tracked by git. ` +
|
|
181
|
-
`Remove it with \`git rm --cached ${secret.relPath}\` and add it to .gitignore. ` +
|
|
182
|
-
`Prefer environment variables or your OS keychain for provider credentials.`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** Public API surface for programmatic use. */
|
|
2
|
-
|
|
3
|
-
export type {
|
|
4
|
-
Config,
|
|
5
|
-
ProviderConfig,
|
|
6
|
-
ProviderName,
|
|
7
|
-
TargetLanguage,
|
|
8
|
-
SourceFile,
|
|
9
|
-
SourceKind,
|
|
10
|
-
DiscoveryResult,
|
|
11
|
-
} from "./types.ts";
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
loadConfig,
|
|
15
|
-
validateConfig,
|
|
16
|
-
defaultConfigJson,
|
|
17
|
-
DEFAULT_CONFIG,
|
|
18
|
-
CONFIG_FILENAME,
|
|
19
|
-
ConfigError,
|
|
20
|
-
} from "./config.ts";
|
|
21
|
-
|
|
22
|
-
export { discover, secretsTrackedError } from "./discovery.ts";
|
package/src/types.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for human-to-code.
|
|
3
|
-
*
|
|
4
|
-
* The pipeline is: .human --(LLM)--> .strict.human --(deterministic)--> code
|
|
5
|
-
* These types describe the deterministic core (config + discovery) only; the
|
|
6
|
-
* strict DSL and code generators are added in later build steps.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type TargetLanguage = "typescript" | "javascript" | "python";
|
|
10
|
-
|
|
11
|
-
export type ProviderName =
|
|
12
|
-
| "openai"
|
|
13
|
-
| "anthropic"
|
|
14
|
-
| "ollama"
|
|
15
|
-
| "grok"
|
|
16
|
-
| "gemini";
|
|
17
|
-
|
|
18
|
-
export interface ProviderConfig {
|
|
19
|
-
/** Which LLM provider drives the human -> strict step. */
|
|
20
|
-
name: ProviderName;
|
|
21
|
-
/** Model id. Configurable; defaults are current, not dated snapshots. */
|
|
22
|
-
model: string;
|
|
23
|
-
/** Optional override endpoint (e.g. Ollama Cloud). Must be https. */
|
|
24
|
-
baseUrl?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Config {
|
|
28
|
-
/** Target language for the deterministic code generator. */
|
|
29
|
-
language: TargetLanguage;
|
|
30
|
-
/** Glob-free directory/file names to skip during discovery (a denylist). */
|
|
31
|
-
filesToIgnore: string[];
|
|
32
|
-
/**
|
|
33
|
-
* If true, non-.human files carrying `@Human:` directives may also be
|
|
34
|
-
* processed. Off by default — it widens the surface area (see plan §1.6).
|
|
35
|
-
*/
|
|
36
|
-
allowNonHumanFiles: boolean;
|
|
37
|
-
/** Provider used only for the human -> strict step. */
|
|
38
|
-
provider: ProviderConfig;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Classification of a source file found during discovery. */
|
|
42
|
-
export type SourceKind =
|
|
43
|
-
/** A hand-written or generated strict IR file: `foo.strict.human`. */
|
|
44
|
-
| "strict"
|
|
45
|
-
/** A natural-language source file: `foo.human` (not `.strict.human`). */
|
|
46
|
-
| "human"
|
|
47
|
-
/** Reserved special files, never compiled and never sent to a provider. */
|
|
48
|
-
| "config"
|
|
49
|
-
| "secrets";
|
|
50
|
-
|
|
51
|
-
export interface SourceFile {
|
|
52
|
-
/** Absolute path. */
|
|
53
|
-
absPath: string;
|
|
54
|
-
/** Path relative to the discovery root, using forward slashes. */
|
|
55
|
-
relPath: string;
|
|
56
|
-
kind: SourceKind;
|
|
57
|
-
/**
|
|
58
|
-
* For a `.human` file, the sibling `.strict.human` path that would take
|
|
59
|
-
* precedence if it exists. Undefined for non-human kinds.
|
|
60
|
-
*/
|
|
61
|
-
strictSibling?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface DiscoveryResult {
|
|
65
|
-
root: string;
|
|
66
|
-
/** Natural-language sources eligible for the human -> strict step. */
|
|
67
|
-
human: SourceFile[];
|
|
68
|
-
/** Strict IR sources (hand-written ones take precedence over generated). */
|
|
69
|
-
strict: SourceFile[];
|
|
70
|
-
/** The secrets file, if present. Never compiled, never sent to a provider. */
|
|
71
|
-
secrets?: SourceFile;
|
|
72
|
-
/** Files skipped by ignore rules, for reporting under --dry-run. */
|
|
73
|
-
ignoredCount: number;
|
|
74
|
-
}
|
package/test/config.test.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
validateConfig,
|
|
8
|
-
loadConfig,
|
|
9
|
-
ConfigError,
|
|
10
|
-
DEFAULT_CONFIG,
|
|
11
|
-
CONFIG_FILENAME,
|
|
12
|
-
} from "../src/config.ts";
|
|
13
|
-
|
|
14
|
-
test("validateConfig fills defaults from an empty object", () => {
|
|
15
|
-
const c = validateConfig({});
|
|
16
|
-
assert.equal(c.language, "typescript");
|
|
17
|
-
assert.equal(c.provider.name, "anthropic");
|
|
18
|
-
assert.equal(c.provider.model, "claude-opus-4-8");
|
|
19
|
-
assert.equal(c.allowNonHumanFiles, false);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("validateConfig accepts a valid language", () => {
|
|
23
|
-
assert.equal(validateConfig({ language: "python" }).language, "python");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("validateConfig rejects an unknown language", () => {
|
|
27
|
-
assert.throws(() => validateConfig({ language: "rust" }), ConfigError);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("provider model defaults per provider", () => {
|
|
31
|
-
const c = validateConfig({ provider: { name: "openai" } });
|
|
32
|
-
assert.equal(c.provider.model, "gpt-4o");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("validateConfig rejects an unknown provider", () => {
|
|
36
|
-
assert.throws(
|
|
37
|
-
() => validateConfig({ provider: { name: "cohere" } }),
|
|
38
|
-
ConfigError,
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("baseUrl must be https", () => {
|
|
43
|
-
assert.throws(
|
|
44
|
-
() =>
|
|
45
|
-
validateConfig({
|
|
46
|
-
provider: { name: "ollama", baseUrl: "http://ollama.com" },
|
|
47
|
-
}),
|
|
48
|
-
ConfigError,
|
|
49
|
-
);
|
|
50
|
-
const ok = validateConfig({
|
|
51
|
-
provider: { name: "ollama", baseUrl: "https://ollama.com" },
|
|
52
|
-
});
|
|
53
|
-
assert.equal(ok.provider.baseUrl, "https://ollama.com");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("validateConfig rejects non-object root", () => {
|
|
57
|
-
assert.throws(() => validateConfig([]), ConfigError);
|
|
58
|
-
assert.throws(() => validateConfig("nope"), ConfigError);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("loadConfig returns defaults when no file exists", async () => {
|
|
62
|
-
const dir = await mkdtemp(join(tmpdir(), "h2c-cfg-"));
|
|
63
|
-
try {
|
|
64
|
-
const { config, fromFile } = await loadConfig(dir);
|
|
65
|
-
assert.equal(fromFile, false);
|
|
66
|
-
assert.deepEqual(config, DEFAULT_CONFIG);
|
|
67
|
-
} finally {
|
|
68
|
-
await rm(dir, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("loadConfig reads and validates a config file", async () => {
|
|
73
|
-
const dir = await mkdtemp(join(tmpdir(), "h2c-cfg-"));
|
|
74
|
-
try {
|
|
75
|
-
await writeFile(
|
|
76
|
-
join(dir, CONFIG_FILENAME),
|
|
77
|
-
JSON.stringify({ language: "javascript", provider: { name: "gemini" } }),
|
|
78
|
-
);
|
|
79
|
-
const { config, fromFile } = await loadConfig(dir);
|
|
80
|
-
assert.equal(fromFile, true);
|
|
81
|
-
assert.equal(config.language, "javascript");
|
|
82
|
-
assert.equal(config.provider.name, "gemini");
|
|
83
|
-
assert.equal(config.provider.model, "gemini-2.5-pro");
|
|
84
|
-
} finally {
|
|
85
|
-
await rm(dir, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("loadConfig surfaces invalid JSON as ConfigError", async () => {
|
|
90
|
-
const dir = await mkdtemp(join(tmpdir(), "h2c-cfg-"));
|
|
91
|
-
try {
|
|
92
|
-
await writeFile(join(dir, CONFIG_FILENAME), "{ not json ");
|
|
93
|
-
await assert.rejects(() => loadConfig(dir), ConfigError);
|
|
94
|
-
} finally {
|
|
95
|
-
await rm(dir, { recursive: true, force: true });
|
|
96
|
-
}
|
|
97
|
-
});
|
package/test/discovery.test.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, symlink, rm } from "node:fs/promises";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { discover, secretsTrackedError } from "../src/discovery.ts";
|
|
8
|
-
|
|
9
|
-
async function makeFixture(): Promise<string> {
|
|
10
|
-
const root = await mkdtemp(join(tmpdir(), "h2c-disc-"));
|
|
11
|
-
await writeFile(join(root, "a.human"), "print hello");
|
|
12
|
-
await writeFile(join(root, "a.strict.human"), "fn printHello");
|
|
13
|
-
await writeFile(join(root, "b.human"), "print bye"); // no strict sibling
|
|
14
|
-
await writeFile(join(root, "secrets.human"), "API Key: sk-x");
|
|
15
|
-
await writeFile(join(root, "config.human"), "Language: typescript");
|
|
16
|
-
await writeFile(join(root, "human-to-code.config.json"), "{}");
|
|
17
|
-
await mkdir(join(root, "node_modules"));
|
|
18
|
-
await writeFile(join(root, "node_modules", "dep.human"), "ignored");
|
|
19
|
-
await mkdir(join(root, "sub"));
|
|
20
|
-
await writeFile(join(root, "sub", "c.human"), "nested");
|
|
21
|
-
try {
|
|
22
|
-
await symlink(join(root, "sub"), join(root, "linkdir"), "dir");
|
|
23
|
-
} catch {
|
|
24
|
-
// symlinks may be unavailable; the rest of the assertions still hold.
|
|
25
|
-
}
|
|
26
|
-
return root;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("discover classifies files and applies ignore rules", async () => {
|
|
30
|
-
const root = await makeFixture();
|
|
31
|
-
try {
|
|
32
|
-
const r = await discover(root, ["node_modules", ".git", "dist"]);
|
|
33
|
-
|
|
34
|
-
const humanPaths = r.human.map((f) => f.relPath).sort();
|
|
35
|
-
assert.deepEqual(humanPaths, ["a.human", "b.human", "sub/c.human"]);
|
|
36
|
-
|
|
37
|
-
assert.deepEqual(
|
|
38
|
-
r.strict.map((f) => f.relPath),
|
|
39
|
-
["a.strict.human"],
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
// node_modules/dep.human is ignored.
|
|
43
|
-
assert.ok(!humanPaths.includes("node_modules/dep.human"));
|
|
44
|
-
|
|
45
|
-
// The symlinked dir must not be followed (no duplicate c.human).
|
|
46
|
-
assert.ok(!humanPaths.includes("linkdir/c.human"));
|
|
47
|
-
|
|
48
|
-
// secrets.human is surfaced separately, never as a human source.
|
|
49
|
-
assert.ok(r.secrets);
|
|
50
|
-
assert.equal(r.secrets?.relPath, "secrets.human");
|
|
51
|
-
|
|
52
|
-
// strictSibling wiring.
|
|
53
|
-
const a = r.human.find((f) => f.relPath === "a.human");
|
|
54
|
-
assert.equal(a?.strictSibling, "a.strict.human");
|
|
55
|
-
const b = r.human.find((f) => f.relPath === "b.human");
|
|
56
|
-
assert.equal(b?.strictSibling, "b.strict.human");
|
|
57
|
-
} finally {
|
|
58
|
-
await rm(root, { recursive: true, force: true });
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("secretsTrackedError returns null when not in a git repo", async () => {
|
|
63
|
-
const root = await makeFixture();
|
|
64
|
-
try {
|
|
65
|
-
const r = await discover(root, ["node_modules"]);
|
|
66
|
-
assert.ok(r.secrets);
|
|
67
|
-
assert.equal(secretsTrackedError(r.secrets!), null);
|
|
68
|
-
} finally {
|
|
69
|
-
await rm(root, { recursive: true, force: true });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("secretsTrackedError refuses when secrets.human is git-tracked", async () => {
|
|
74
|
-
const root = await makeFixture();
|
|
75
|
-
try {
|
|
76
|
-
assert.equal(
|
|
77
|
-
spawnSync("git", ["-C", root, "init", "-q"]).status,
|
|
78
|
-
0,
|
|
79
|
-
"git init failed",
|
|
80
|
-
);
|
|
81
|
-
// Staging is enough for `git ls-files` to consider it tracked.
|
|
82
|
-
spawnSync("git", ["-C", root, "add", "secrets.human"]);
|
|
83
|
-
|
|
84
|
-
const r = await discover(root, ["node_modules"]);
|
|
85
|
-
assert.ok(r.secrets);
|
|
86
|
-
const msg = secretsTrackedError(r.secrets!);
|
|
87
|
-
assert.ok(msg && msg.includes("Refusing to run"));
|
|
88
|
-
} finally {
|
|
89
|
-
await rm(root, { recursive: true, force: true });
|
|
90
|
-
}
|
|
91
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "nodenext",
|
|
5
|
-
"moduleResolution": "nodenext",
|
|
6
|
-
"lib": ["esnext"],
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noUncheckedIndexedAccess": true,
|
|
10
|
-
"verbatimModuleSyntax": true,
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"noEmit": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src", "test"]
|
|
17
|
-
}
|