diff --git a/.configs/.prettierrc b/.configs/.prettierrc index af4250c..f84f51b 100644 --- a/.configs/.prettierrc +++ b/.configs/.prettierrc @@ -17,6 +17,13 @@ "options": { "proseWrap": "preserve" } + }, + { + "files": "*.jsonv", + "options": { + "parser": "jsonv" + } } - ] + ], + "plugins": ["@cldmv/prettier-plugin-jsonv"] } diff --git a/.configs/eslint.config.mjs b/.configs/eslint.config.mjs index cebb305..2fcff2f 100644 --- a/.configs/eslint.config.mjs +++ b/.configs/eslint.config.mjs @@ -1,12 +1,16 @@ import js from "@eslint/js"; import globals from "globals"; import json from "@eslint/json"; +import jsonvPlugin from "@cldmv/eslint-plugin-jsonv"; import markdown from "@eslint/markdown"; +import css from "@eslint/css"; import { defineConfig } from "eslint/config"; export default defineConfig([ { ignores: [ + "tmp/**", + "trash/**", "node_modules/**", "dist/**", "build/**", @@ -15,7 +19,21 @@ export default defineConfig([ ".vscode/**", "coverage/**", "reference/**", - "**/package-lock.json" + "*.min.*", + "**/package-lock.json", + // Copy file patterns + "*copy/", + "*copy (*)/", + "*copy */", + "*copy.*", + "*copy (*).*", + "*copy *.*", + "**/*copy/", + "**/*copy (*)/", + "**/*copy */", + "**/*copy.*", + "**/*copy (*).*", + "**/*copy *.*" ] }, { @@ -35,7 +53,7 @@ export default defineConfig([ } }, { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, - { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.node } } }, + { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.node, ...globals.browser } } }, { files: ["tests/**/*.test.vitest.mjs"], languageOptions: { @@ -54,13 +72,17 @@ export default defineConfig([ }, { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, { files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc", extends: ["json/recommended"] }, + { files: ["**/*.json5"], plugins: { json }, language: "json/json5", extends: ["json/recommended"] }, + { files: ["**/*.jsonv"], plugins: { jsonv: jsonvPlugin }, language: "jsonv/jsonv", ...jsonvPlugin.configs.recommended }, { files: ["**/*.md"], plugins: { markdown }, language: "markdown/gfm", extends: ["markdown/recommended"], rules: { + // GitHub alerts like [!NOTE]/[!WARNING] are valid but trip this rule. "markdown/no-missing-label-refs": "off" } - } + }, + { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] } ]); diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..92b4b71 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Dependencies + generated/build output (not hand-authored) +node_modules/ +dist/ +build/ +coverage/ +tmp/ +trash/ +reference/ +package-lock.json +# minified / compact generated files of any extension (*.min.js/.css/.json/.map…) +*.min.* + +# YAML — CI workflows are deliberately 4-space indented; prettier (tabWidth 2, +# and YAML disallows tabs) would reindent every file 4->2 for no real gain. +*.yml +*.yaml diff --git a/docs/use-case-private-tests.md b/docs/use-case-private-tests.md index 164def9..0f43c55 100644 --- a/docs/use-case-private-tests.md +++ b/docs/use-case-private-tests.md @@ -10,7 +10,7 @@ The classical clean-room reimplementation defense — used most famously by Phoe The defense is strong when the dirty team is genuinely good at abstraction. It is weak when the "spec" they hand off is mostly the original code transliterated into prose — that intermediate spec is itself a derivative work, and so is the downstream output. Humans trained as engineers tend to be careful at this; large language models tend not to be, paraphrasing more than abstracting. -Comprehensive tests bypass the dirty team entirely. They are already a behavioral specification — that's their entire purpose — and they were written to be implementation-independent. A test file is *not* a derivative work of the implementation in any meaningful sense; courts have held repeatedly that purely functional descriptions, API surfaces, and behavioral specifications are not protectable expression (Google v. Oracle 2021, Computer Associates v. Altai 1992, Sega v. Accolade 1992). The output produced by an AI given test files as input is not, on its face, substantially similar to the original code — especially if the reimplementation is in a different programming language. +Comprehensive tests bypass the dirty team entirely. They are already a behavioral specification — that's their entire purpose — and they were written to be implementation-independent. A test file is _not_ a derivative work of the implementation in any meaningful sense; courts have held repeatedly that purely functional descriptions, API surfaces, and behavioral specifications are not protectable expression (Google v. Oracle 2021, Computer Associates v. Altai 1992, Sega v. Accolade 1992). The output produced by an AI given test files as input is not, on its face, substantially similar to the original code — especially if the reimplementation is in a different programming language. The threat is therefore: @@ -45,7 +45,7 @@ Several alternatives were considered for tracking the pinned tests version: - **A submodule with a fake URL** in `.gitmodules`, overridden locally via `.git/config`. Git automation works because the registry entry exists; the URL leak is reduced because the public URL is a non-resolving decoy. But the decoy itself advertises "we are deliberately hiding the real URL," which signals more intent than no entry at all. - **An anonymous gitlink** (gitlink in the tree with no `.gitmodules` entry). Most opaque — the public parent shows the gitlink and SHA but no URL, which could plausibly be a leftover, mistake, or any other innocuous explanation. Loses git's automation, which is the gap this package fills with its hooks. -The choice is partly aesthetic but mostly about how the parent repo presents itself to outside observers. The anonymous gitlink presents minimum signal: an embedded reference whose target is not declared, the way most other features in a repo work (you can see *that* something exists without being told everything about *what* it is). +The choice is partly aesthetic but mostly about how the parent repo presents itself to outside observers. The anonymous gitlink presents minimum signal: an embedded reference whose target is not declared, the way most other features in a repo work (you can see _that_ something exists without being told everything about _what_ it is). ## Licensing strategy diff --git a/messages/setup-dispatcher-missing-symlinks.md b/messages/setup-dispatcher-missing-symlinks.md index 6e846ef..1fdf0b6 100644 --- a/messages/setup-dispatcher-missing-symlinks.md +++ b/messages/setup-dispatcher-missing-symlinks.md @@ -28,6 +28,7 @@ The CLI will then install `git-embedded`'s hook scripts into this repo's `.git/h If you decline the heal, the CLI exits without changes. You can heal manually: **Linux / macOS (symlinks, no elevation needed):** + ```bash ln -sf _dispatch /post-checkout ln -sf _dispatch /post-merge @@ -36,6 +37,7 @@ ln -sf _dispatch /reference-transaction ``` **Windows (symlinks, run from an elevated Command Prompt):** + ```cmd mklink "\post-checkout" "\_dispatch" mklink "\post-merge" "\_dispatch" @@ -44,6 +46,7 @@ mklink "\reference-transaction" "\_dispatch" ``` **Windows (hard links, no elevation needed — alternative if you don't want to elevate):** + ```cmd mklink /H "\post-checkout" "\_dispatch" mklink /H "\post-merge" "\_dispatch" diff --git a/messages/setup-pre-commit.md b/messages/setup-pre-commit.md index 2d4be50..ccd6923 100644 --- a/messages/setup-pre-commit.md +++ b/messages/setup-pre-commit.md @@ -31,8 +31,9 @@ repos: entry: npx git-embedded run-hook reference-transaction language: system pass_filenames: false - stages: [reference-transaction] # support for this stage depends on - # the pre-commit version + stages: + [reference-transaction] # support for this stage depends on + # the pre-commit version verbose: true ``` diff --git a/messages/setup-simple-git-hooks.md b/messages/setup-simple-git-hooks.md index d048f39..54edde2 100644 --- a/messages/setup-simple-git-hooks.md +++ b/messages/setup-simple-git-hooks.md @@ -14,12 +14,12 @@ Edit `package.json` to add `git-embedded`'s invocations to the `simple-git-hooks ```json { - "simple-git-hooks": { - "post-checkout": "npx git-embedded run-hook post-checkout \"$@\"", - "post-merge": "npx git-embedded run-hook post-merge \"$@\"", - "post-rewrite": "npx git-embedded run-hook post-rewrite \"$@\"", - "reference-transaction": "npx git-embedded run-hook reference-transaction \"$@\"" - } + "simple-git-hooks": { + "post-checkout": "npx git-embedded run-hook post-checkout \"$@\"", + "post-merge": "npx git-embedded run-hook post-merge \"$@\"", + "post-rewrite": "npx git-embedded run-hook post-rewrite \"$@\"", + "reference-transaction": "npx git-embedded run-hook reference-transaction \"$@\"" + } } ``` diff --git a/package-lock.json b/package-lock.json index aa551ae..8846c59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/git-embedded", - "version": "1.1.6", + "version": "1.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/git-embedded", - "version": "1.1.6", + "version": "1.1.7", "license": "Apache-2.0", "dependencies": { "@cldmv/slothlet": "^3.7.0", @@ -20,7 +20,11 @@ "git-embedded": "bin/git-embedded.mjs" }, "devDependencies": { + "@cldmv/eslint-plugin-jsonv": "^1.0.3", + "@cldmv/jsonv": "^1.0.2", + "@cldmv/prettier-plugin-jsonv": "^1.0.1", "@cldmv/vitest-runner": "^1.2.0", + "@eslint/css": "^1.4.0", "@eslint/js": "^9.18.0", "@eslint/json": "^2.0.1", "@eslint/markdown": "^8.0.3", @@ -98,6 +102,52 @@ "node": ">=18" } }, + "node_modules/@cldmv/eslint-plugin-jsonv": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cldmv/eslint-plugin-jsonv/-/eslint-plugin-jsonv-1.0.3.tgz", + "integrity": "sha512-P/AHUOaKnOV98qUfbBKgZ85uz3jVRSM/yjc6yKrAjaS5s5BEqpnVxIbQYcvfnPxopcA/IsCsedUjl9E7s3bM8A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/shinrai" + }, + "peerDependencies": { + "@cldmv/jsonv": "^1.0.2" + } + }, + "node_modules/@cldmv/jsonv": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@cldmv/jsonv/-/jsonv-1.0.2.tgz", + "integrity": "sha512-YfJtIoiBf43eeiqqtUYcF0RJUJfpS+Ixzdm8yaQnLG7xlOG9xHSMcqfkPvgjoJiA3hPmHbAeAo4dSJZmwS6orQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/shinrai" + } + }, + "node_modules/@cldmv/prettier-plugin-jsonv": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@cldmv/prettier-plugin-jsonv/-/prettier-plugin-jsonv-1.0.1.tgz", + "integrity": "sha512-TsgxoXQhGO7aNABbKSd/xcuwrrNtvhvzVtWkiuH6QHOE346x3iZsHtk1IqHq9xPv8Knv/C+53ScYaFjAodhUSQ==", + "dev": true, + "license": "Apache-2.0", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/shinrai" + }, + "peerDependencies": { + "@cldmv/jsonv": "^1.0.0", + "prettier": "^3.0.0" + } + }, "node_modules/@cldmv/slothlet": { "version": "3.12.2", "resolved": "https://registry.npmjs.org/@cldmv/slothlet/-/slothlet-3.12.2.tgz", @@ -309,6 +359,35 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@eslint/css/-/css-1.4.0.tgz", + "integrity": "sha512-OnRNKgnbX+tufPuZc2kOJS+v1iIARvfJHRNEAVwN77DBpojl+rGjEP8NKTL6pEZ7BR+HtwIuSSdrymEFlANGxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "@eslint/css-tree": "^4.0.4", + "@eslint/plugin-kit": "^0.7.2" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/css-tree": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-4.0.5.tgz", + "integrity": "sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.29.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", @@ -2696,6 +2775,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.29.0.tgz", + "integrity": "sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", diff --git a/package.json b/package.json index df4eb16..2a47ac2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/git-embedded", - "version": "1.1.6", + "version": "1.1.7", "description": "Manage embedded git repositories (anonymous gitlinks) without .gitmodules. Provides hooks that restore standard git-command ergonomics for embedded children while keeping the child's origin URL out of the public parent repo.", "type": "module", "license": "Apache-2.0", @@ -87,7 +87,11 @@ "marked-terminal": "^7.3.0" }, "devDependencies": { + "@cldmv/eslint-plugin-jsonv": "^1.0.3", + "@cldmv/jsonv": "^1.0.2", + "@cldmv/prettier-plugin-jsonv": "^1.0.1", "@cldmv/vitest-runner": "^1.2.0", + "@eslint/css": "^1.4.0", "@eslint/js": "^9.18.0", "@eslint/json": "^2.0.1", "@eslint/markdown": "^8.0.3", diff --git a/src/api/cli/install-hooks.mjs b/src/api/cli/install-hooks.mjs index 656d5a2..a87d574 100644 --- a/src/api/cli/install-hooks.mjs +++ b/src/api/cli/install-hooks.mjs @@ -9,11 +9,7 @@ export const spec = { ["--yes", "Skip confirmation prompts; assume yes for any modification."], ["--dispatcher-dir ", "Override the default dispatcher directory (~/.config/git/hooks)."] ], - examples: [ - "$ git-embedded install-hooks", - "$ git-embedded install-hooks --no-symlinks", - "$ git-embedded install-hooks --yes" - ] + examples: ["$ git-embedded install-hooks", "$ git-embedded install-hooks --no-symlinks", "$ git-embedded install-hooks --yes"] }; export async function run(opts = {}) { @@ -72,7 +68,8 @@ async function healAndInstall(result, opts) { { noSymlinks: opts.noSymlinks } ); self.report.success(`Healed ${linkResult.created.length} entries`); - if (linkResult.fallbackToCopy.length > 0) self.report.warn(`Filesystem fallback to copy for ${linkResult.fallbackToCopy.length} entries`); + if (linkResult.fallbackToCopy.length > 0) + self.report.warn(`Filesystem fallback to copy for ${linkResult.fallbackToCopy.length} entries`); } catch (err) { if (err instanceof CancelledByUser) { self.report.error(err.message); diff --git a/src/api/cli/restore.mjs b/src/api/cli/restore.mjs index a0714c9..174b61c 100644 --- a/src/api/cli/restore.mjs +++ b/src/api/cli/restore.mjs @@ -21,7 +21,8 @@ export const spec = { }; const LABEL = { - restored: (r) => `${r.dryRun ? "would restore" : "restored"} ${r.path} from ${r.source} (${r.url})${r.branch ? ` on branch ${r.branch}` : ""}`, + restored: (r) => + `${r.dryRun ? "would restore" : "restored"} ${r.path} from ${r.source} (${r.url})${r.branch ? ` on branch ${r.branch}` : ""}`, "already-present": (r) => `${r.path} already present`, skipped: (r) => `${r.path} skipped`, unresolved: (r) => `${r.path} unresolved${r.note ? ` — ${r.note}` : ""}`, diff --git a/src/api/commander/custom-help.mjs b/src/api/commander/custom-help.mjs index 43bf2cc..f4b6e36 100644 --- a/src/api/commander/custom-help.mjs +++ b/src/api/commander/custom-help.mjs @@ -164,14 +164,12 @@ export function makeCustomHelp(HelpClass, deps) { const cmdColor = indent === 1 ? chalk.cyan : chalk.blueBright; output.push(`${pad}${color(cmdColor, term)}`); if (aliases.length) { - wrapTextWithHangingIndent(aliases.join(", "), indent + 1, "Aliases", (s) => color(chalk.yellow.italic, s)).forEach( - (l) => output.push(l) + wrapTextWithHangingIndent(aliases.join(", "), indent + 1, "Aliases", (s) => color(chalk.yellow.italic, s)).forEach((l) => + output.push(l) ); } if (desc) { - wrapTextWithHangingIndent(desc, indent + 1, "Description", (s) => color(chalk.gray.italic, s)).forEach((l) => - output.push(l) - ); + wrapTextWithHangingIndent(desc, indent + 1, "Description", (s) => color(chalk.gray.italic, s)).forEach((l) => output.push(l)); } output.push(""); diff --git a/src/api/detect/dispatcher.mjs b/src/api/detect/dispatcher.mjs index 147605d..147c41a 100644 --- a/src/api/detect/dispatcher.mjs +++ b/src/api/detect/dispatcher.mjs @@ -22,11 +22,7 @@ export const STANDARD_HOOK_NAMES = [ const DISPATCHER_MARKER = "# git-embedded-compatible dispatcher"; -const CHAIN_PATTERNS = [ - /exec\s+"?\$repo_hook"?/, - /exec\s+"?\$\{?repo_hook\}?"?/, - /exec\s+"?\$git_dir\/hooks\/\$hook"?/ -]; +const CHAIN_PATTERNS = [/exec\s+"?\$repo_hook"?/, /exec\s+"?\$\{?repo_hook\}?"?/, /exec\s+"?\$git_dir\/hooks\/\$hook"?/]; function readFull(p) { try { diff --git a/tests/embedded-coverage.test.vitest.mjs b/tests/embedded-coverage.test.vitest.mjs index 8d65b85..964c4b8 100644 --- a/tests/embedded-coverage.test.vitest.mjs +++ b/tests/embedded-coverage.test.vitest.mjs @@ -270,11 +270,7 @@ describe("manifest.build entry filtering", () => { }); it("drops null and url-less entries, keeping only entries with a url", () => { - const manifest = api.embedded.manifest.build([ - null, - { path: "no-url" }, - { path: "keep", url: "ssh://h/keep.git", branch: "main" } - ]); + const manifest = api.embedded.manifest.build([null, { path: "no-url" }, { path: "keep", url: "ssh://h/keep.git", branch: "main" }]); expect(Object.keys(manifest.children)).toEqual(["keep"]); expect(manifest.children.keep).toEqual({ url: "ssh://h/keep.git", branch: "main" }); }); @@ -468,7 +464,9 @@ describe("sync edge branches", () => { // it a sync-failed anomaly. fs.rmSync(path.join(fresh, "tests"), { recursive: true, force: true }); const { results, exitCode } = api.embedded.sync({ cwd: fresh, paths: ["tests"] }); - expect(results).toEqual([{ path: "tests", sha: expect.any(String), branch: null, note: "not present on disk — run restore", outcome: "no-repo" }]); + expect(results).toEqual([ + { path: "tests", sha: expect.any(String), branch: null, note: "not present on disk — run restore", outcome: "no-repo" } + ]); expect(exitCode).toBe(0); });