declapract-typescript-ehmpathy 0.49.8 → 0.49.9

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,6 @@
1
+ # .note = declarations dir is symlinked by the test
2
+ # .why = `rhachet-only` deliberately omits the `git` practice, to prove rhachet
3
+ # declares the ignore for the dir rhachet writes without help from a
4
+ # neighbor practice
5
+ declarations: 'declarations/declapract.declare.yml'
6
+ useCase: rhachet-only
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test-project-rhachet-only",
3
+ "version": "1.0.0"
4
+ }
@@ -0,0 +1,9 @@
1
+ # .note = declarations dir is symlinked by the test
2
+ # .note = NO `variables:` block. the rule's top gotcha is that apply evaluates the
3
+ # whole practice, so every `@declapract{variable.*}` it references must be
4
+ # defined here. both `git` and `rhachet` reference ZERO -- verified by grep,
5
+ # and by this fixture, which applies clean without any. a variables block
6
+ # copied from another fixture "to be safe" would be a claim the practices
7
+ # make demands they do not.
8
+ declarations: 'declarations/declapract.declare.yml'
9
+ useCase: typescript-project
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test-project-gitignore",
3
+ "version": "1.0.0"
4
+ }
@@ -0,0 +1,51 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`gitignore practices given: [case1] a repo whose .gitignore lacks both cache dirs when: [t0] before any apply then: the input file matches snapshot: .gitignore -- before 1`] = `
4
+ "dist
5
+ coverage
6
+ .idea
7
+ node_modules
8
+ "
9
+ `;
10
+
11
+ exports[`gitignore practices given: [case1] a repo whose .gitignore lacks both cache dirs when: [t3] both practices are applied a second time then: the settled file matches snapshot: .gitignore -- settled 1`] = `
12
+ "*.bak.*
13
+ *.local.json
14
+ *.log
15
+ *.tsbuildinfo
16
+ .agent/.cache/
17
+ .artifact
18
+ .cache/
19
+ .env
20
+ .idea
21
+ .log/slowtest/
22
+ .serverless
23
+ .temp
24
+ .terraform
25
+ .terraform.lock
26
+ .vscode
27
+ .yalc
28
+ coverage
29
+ dist
30
+ node_modules
31
+ !.test*/**/node_modules
32
+ !.test*/**/node_modules/**
33
+ "
34
+ `;
35
+
36
+ exports[`gitignore practices given: [case2] a repo whose use case has rhachet but NOT git when: [t0] before any apply then: the input file matches snapshot: .gitignore -- rhachet only -- before 1`] = `
37
+ "dist
38
+ node_modules
39
+ .idea
40
+ "
41
+ `;
42
+
43
+ exports[`gitignore practices given: [case2] a repo whose use case has rhachet but NOT git when: [t1] the rhachet practice is applied then: the settled file matches snapshot: .gitignore -- rhachet only 1`] = `
44
+ ".agent/.cache/
45
+ .idea
46
+ dist
47
+ node_modules
48
+ !.test*/**/node_modules
49
+ !.test*/**/node_modules/**
50
+ "
51
+ `;
@@ -0,0 +1,23 @@
1
+ the repo-root `.gitignore` and `.gitattributes`
2
+
3
+ 📖 several practices declare the `.gitignore`. the contract they share — one shared algorithm, the
4
+ ordered tail, what a new declarer owes — is stated once at
5
+ `.agent/repo=.this/role=any/briefs/howto.declare-the-repo-gitignore.md`. read that before you add
6
+ a line here.
7
+
8
+ ## `.agent/.cache/` is declared here and by the rhachet practice
9
+
10
+ deliberate, and neither is redundant. rhachet owns the dir in the causal sense — it creates it —
11
+ which is a different claim from which practices declare the ignore for it.
12
+
13
+ this line stays because `app-react-native-expo` (`src/useCases.yml`) takes `git` **without**
14
+ `rhachet`, so for those repos it is the only explicit source of the ignore. `.cache/` happens to
15
+ cover `.agent/.cache/` at any depth, but that coverage is implicit — narrow `.cache/` and it is
16
+ gone. the same caution sits in `.gitignore.declapract.ts`, at the point of edit.
17
+
18
+ ## this repo consumes this practice
19
+
20
+ `declapract.use.yml` sets `useCase: npm-package`, which extends `typescript-project`, which
21
+ carries `git`. so `[case3]` in `src/practices/git/.declapract.integration.test.ts` checks this
22
+ repo's own `.gitignore` against the declaration — add a line to `ignoresSortable` and it goes red
23
+ until the practice is applied here too.
@@ -1,14 +1,40 @@
1
1
  import type { FileCheckFunction, FileFixFunction } from 'declapract';
2
- import { dedupe } from 'domain-objects';
3
2
  import expect from 'expect';
4
3
 
4
+ import { defineExpectedGitignoreContents } from '../../../utils/defineExpectedGitignoreContents';
5
+
5
6
  /**
6
7
  * ignores that can be sorted alphabetically (no order dependencies)
8
+ *
9
+ * .note = `.agent/.cache/` is declared HERE and by
10
+ * `src/practices/rhachet/best-practice/.gitignore.declapract.ts`. that is not drift, and
11
+ * the line must not be "cleaned up" as a duplicate of rhachet's -- rhachet OWNS that dir
12
+ * in the causal sense (it creates it), which is a different claim from which practices
13
+ * declare the ignore for it.
14
+ *
15
+ * two facts carry the load here:
16
+ * - `app-react-native-expo` (`src/useCases.yml`) takes `git` WITHOUT `rhachet`, so for
17
+ * every repo on that use case THIS line is the only explicit source of the ignore
18
+ * - `.cache/` below happens to cover `.agent/.cache/` at any depth (a gitignore pattern
19
+ * with no interior separator is unanchored -- measured with `git check-ignore -v`), so
20
+ * a reader sees the overlap and reads it as redundant. that coverage is IMPLICIT --
21
+ * narrow `.cache/` and it is gone
22
+ *
23
+ * so removal here would un-ignore the agent cache for every git-without-rhachet repo, on
24
+ * the day `.cache/` is next edited, with no line left to say why. if this ever moves, it
25
+ * must land in a practice that still reaches every `git` consumer.
26
+ *
27
+ * .note = this list is NOT sorted here, and that is deliberate. `defineExpectedGitignoreContents`
28
+ * sorts the union of these lines with the repo's own, so a `.sort()` at this site would
29
+ * be a second sort that changes no output -- while it mutates a module-cached array in
30
+ * place. `readonly` forbids that mutation outright rather than merely discourages it
31
+ * (`rule.prefer.prevent-over-correct`), so the source may group lines by what they are
32
+ * for, with their comments beside them, and the one sort that matters stays in one place.
7
33
  */
8
- const ignoresSortable = [
34
+ const ignoresSortable: readonly string[] = [
9
35
  '*.log',
10
36
  '*.tsbuildinfo',
11
- '.agent/.cache/', // agent cache (trash dir, temp files)
37
+ '.agent/.cache/', // agent cache -- kept even though rhachet declares it too; see .note above
12
38
  '.artifact', // deployment artifacts from `simple-artifact-builder` are produced here
13
39
  '.cache/', // cache directories from various tools
14
40
  '.env',
@@ -23,44 +49,44 @@ const ignoresSortable = [
23
49
  '*.bak.*', // backup files
24
50
  'coverage',
25
51
  'dist',
26
- ].sort();
52
+ ];
27
53
 
28
54
  /**
29
- * ignores that must appear in a specific order (negations must follow their targets)
30
- *
31
- * note
32
- * - node_modules under .test* dirs are test fixtures (cloned repos via genTempDir)
33
- * - gitignore negation requires the negated pattern to come after the original
55
+ * .what = emits the `.gitignore` this practice declares, given the repo's extant one
56
+ * .why = one algorithm serves both `check` and `fix`, so the gate and the repair can
57
+ * never disagree about what the file should hold.
58
+ * .why = the algorithm AND the ordered tail are imported, not declared here, because this
59
+ * practice is not the only declarer of the repo-root `.gitignore`. declarers converge
60
+ * only while every one of them emits an identical tail by an identical algorithm --
61
+ * so they share one, and the drift that would break convergence cannot be written.
62
+ * which practices those are is a `grep`; a count here would go stale the first time
63
+ * one is added, as it already has once.
34
64
  */
35
- const ignoresOrdered = [
36
- 'node_modules',
37
- '!.test*/**/node_modules',
38
- '!.test*/**/node_modules/**',
39
- ];
40
-
41
- const defineExpectedContents = (contents: string | null): string => {
42
- // parse ignores from file
43
- const ignoresFromFile = contents ? contents.split('\n') : [];
44
-
45
- // separate into sortable vs ordered
46
- const orderedPatterns = new Set(ignoresOrdered);
47
- const ignoresFromFileSortable = ignoresFromFile.filter(
48
- (line) => !!line && !orderedPatterns.has(line),
49
- );
50
-
51
- // combine and sort the sortable ignores
52
- const sortedIgnores = dedupe([...ignoresFromFileSortable, ...ignoresSortable])
53
- .sort()
54
- .filter((line) => !!line);
55
-
56
- // append ordered ignores at the end (order preserved)
57
- return [...sortedIgnores, ...ignoresOrdered, ''].join('\n');
58
- };
65
+ const defineExpectedContents = (contents: string | null): string =>
66
+ defineExpectedGitignoreContents({ contents, ignoresSortable });
59
67
 
68
+ /**
69
+ * .what = throws when the repo's `.gitignore` does not carry what this practice declares
70
+ * .why = `check` is the GATE: declapract runs `fix` for a file only when `check`
71
+ * rejects it. so a line added to `ignoresSortable` above reaches a consumer
72
+ * repo only because this throws on the file that lacks it.
73
+ * .note = best-practice semantics are inverted from what a reader expects -- a THROW
74
+ * means the practice is violated, a return means it is followed. see
75
+ * `.agent/repo=.this/role=any/briefs/declapract-check-semantics.md`.
76
+ */
60
77
  export const check: FileCheckFunction = (contents) => {
61
78
  expect(contents).toEqual(defineExpectedContents(contents));
62
79
  };
63
80
 
81
+ /**
82
+ * .what = unions this practice's declared ignores into the repo's `.gitignore`
83
+ * .why = a findsert by construction -- a line already present stays present once,
84
+ * and a repo's own custom ignores survive, because the file's extant lines
85
+ * are unioned in before the sort.
86
+ * .note = a fixed point: `fix(fix(x)) === fix(x)`, and `check` passes on its own
87
+ * output -- without both, `declapract fix` would rewrite this file forever.
88
+ * clamped by `.gitignore.declapract.test.ts` -> `idempotency`.
89
+ */
64
90
  export const fix: FileFixFunction = (contents) => {
65
91
  return { contents: defineExpectedContents(contents) };
66
92
  };
@@ -0,0 +1,32 @@
1
+ rhachet boots roles, and role boots write scratch state into `.agent/.cache/`
2
+
3
+ so this practice declares the gitignore line for that dir — the practice that creates the scratch
4
+ declares its ignore, rather than a neighbor practice a repo may not have taken.
5
+
6
+ 📖 several practices declare this same `.gitignore`. the contract they share — one shared
7
+ algorithm, the ordered tail, what a new declarer owes — is stated once at
8
+ `.agent/repo=.this/role=any/briefs/howto.declare-the-repo-gitignore.md`. read that before you add
9
+ a line here.
10
+
11
+ ## it declares only `.agent/.cache/`, not `.cache/`
12
+
13
+ `.cache/` is generic tool scratch and belongs to the `git` practice. a practice declares the
14
+ ignore for the dirs it creates.
15
+
16
+ ⚠️ **"rhachet owns it" does not mean "git should drop it".** the `git` practice declares
17
+ `.agent/.cache/` too, and that line stays: `app-react-native-expo` (`src/useCases.yml`) takes
18
+ `git` **without** `rhachet`, so for those repos git's line is the only explicit source of the
19
+ ignore.
20
+
21
+ ## the end-to-end proof lives under the git practice
22
+
23
+ `src/practices/git/.declapract.integration.test.ts` → `[case2] a repo whose use case has rhachet
24
+ but NOT git`.
25
+
26
+ that is the scenario which justifies this declaration at all. it clones a demo repo whose use case
27
+ omits `git`, applies **this** practice alone, and asserts the agent cache path is ignored with no
28
+ help from git, that `.cache/` is correctly absent, that the negation is honored by git itself, and
29
+ that a re-apply changes zero bytes.
30
+
31
+ it sits there because one temp repo must hold both declarations to prove they agree. this pointer
32
+ exists so a reader in `src/practices/rhachet/` can find it.
@@ -0,0 +1,64 @@
1
+ import type { FileCheckFunction, FileFixFunction } from 'declapract';
2
+ import expect from 'expect';
3
+
4
+ import { defineExpectedGitignoreContents } from '../../../utils/defineExpectedGitignoreContents';
5
+
6
+ /**
7
+ * ignores that this practice itself owns
8
+ *
9
+ * .why = rhachet boots roles which write scratch state into `.agent/.cache/`. the
10
+ * practice that creates the dir declares the ignore for it, so a repo that
11
+ * takes `rhachet` is never left with cruft it did not ask for:
12
+ * - a worktree teardown is not snagged by untracked scratch
13
+ * - a hurried `git add .` cannot sweep a debug file that holds a secret
14
+ *
15
+ * .note = `.cache/` is generic tool scratch and belongs to the `git` practice, not
16
+ * here. a practice declares the ignore for the dirs IT creates, and no more.
17
+ *
18
+ * .note = this list is NOT sorted here, and that is deliberate. `defineExpectedGitignoreContents`
19
+ * sorts the union of these lines with the repo's own, so a `.sort()` at this site would
20
+ * be a second sort that changes no output -- while it mutates a module-cached array in
21
+ * place. `readonly` forbids that mutation outright rather than merely discourages it
22
+ * (`rule.prefer.prevent-over-correct`).
23
+ */
24
+ const ignoresSortable: readonly string[] = ['.agent/.cache/'];
25
+
26
+ /**
27
+ * .what = emits the `.gitignore` this practice declares, given the repo's extant one
28
+ * .why = the algorithm AND the ordered tail are imported, not declared here, because
29
+ * `src/practices/git/best-practice/.gitignore.declapract.ts` declares this same
30
+ * repo-root file. two declarers converge only while both emit an identical tail
31
+ * by an identical algorithm -- so they share one, and the drift that would break
32
+ * convergence cannot be written.
33
+ * .note = this file declares only `ignoresSortable`, which is the part the two
34
+ * practices legitimately DIFFER on: rhachet owns `.agent/.cache/`, git owns
35
+ * the generic scratch. the shared part is shared; the owned part is owned.
36
+ */
37
+ const defineExpectedContents = (contents: string | null): string =>
38
+ defineExpectedGitignoreContents({ contents, ignoresSortable });
39
+
40
+ /**
41
+ * .what = throws when the repo's `.gitignore` does not carry what this practice declares
42
+ * .why = `check` is the GATE: declapract runs `fix` for a file only when `check`
43
+ * rejects it. so this is what makes an upgrade actually deliver
44
+ * `.agent/.cache/` to a repo that lacks it.
45
+ * .note = best-practice semantics are inverted from what a reader expects -- a THROW
46
+ * means the practice is violated, a return means it is followed. see
47
+ * `.agent/repo=.this/role=any/briefs/declapract-check-semantics.md`.
48
+ */
49
+ export const check: FileCheckFunction = (contents) => {
50
+ expect(contents).toEqual(defineExpectedContents(contents));
51
+ };
52
+
53
+ /**
54
+ * .what = unions this practice's declared ignores into the repo's `.gitignore`
55
+ * .why = a findsert by construction -- a line already present stays present once,
56
+ * and a repo's own custom ignores survive, because the file's extant lines
57
+ * are unioned in before the sort.
58
+ * .note = a fixed point: `fix(fix(x)) === fix(x)`, and `check` passes on its own
59
+ * output -- without both, `declapract fix` would rewrite this file forever.
60
+ * clamped by `.gitignore.declapract.test.ts` -> `idempotency`.
61
+ */
62
+ export const fix: FileFixFunction = (contents) => {
63
+ return { contents: defineExpectedContents(contents) };
64
+ };
@@ -0,0 +1,84 @@
1
+ import { dedupe } from 'domain-objects';
2
+
3
+ /**
4
+ * .what = the ignores whose POSITION carries load, in the sequence git requires
5
+ * .why = a `.gitignore` negation re-includes a path only when it appears AFTER the
6
+ * pattern it negates. hoist a `!` line above its target and git silently drops
7
+ * the negation -- the file still reads as correct while every test-fixture
8
+ * `node_modules` it protects is re-ignored.
9
+ * .why = it lives HERE, shared, rather than in each declaration, because every
10
+ * practice that declares the repo-root `.gitignore` must emit an IDENTICAL
11
+ * tail or the two converge no longer, and `declapract fix` rewrites the file on
12
+ * every run. one constant makes that drift unrepresentable.
13
+ * .note = `.test*` dirs hold cloned repos (test fixtures via `genTempDir`), whose
14
+ * `node_modules` must stay tracked.
15
+ * .note = `readonly` carries load here, it is not decoration. this array is
16
+ * module-cached and shared by every declarer, so ONE in-place mutation anywhere
17
+ * (`.push`, an in-place `.sort`, `.reverse`) would break convergence for ALL of
18
+ * them at once -- at runtime, with no compiler signal. `readonly` makes that
19
+ * unrepresentable rather than merely discouraged
20
+ * (`rule.prefer.prevent-over-correct`).
21
+ * .note = typed `readonly string[]` rather than `as const`: `as const` narrows the
22
+ * elements to literal types, which would make `orderedPatterns.has(line)` below
23
+ * reject a plain `string`. this form keeps the element type while it forbids the
24
+ * mutation.
25
+ */
26
+ export const ignoresOrderedForGitignore: readonly string[] = [
27
+ 'node_modules',
28
+ '!.test*/**/node_modules',
29
+ '!.test*/**/node_modules/**',
30
+ ];
31
+
32
+ /**
33
+ * .what = emits the `.gitignore` a practice declares, given the repo's extant one
34
+ * .why = a findsert by construction -- the file's own lines are unioned in before the
35
+ * sort, so a repo's custom ignores survive and a line already present stays
36
+ * present once. and it is a fixed point: `fix(fix(x)) === fix(x)`.
37
+ * .why = SHARED across every practice that declares this file. convergence between
38
+ * two declarers rests on the whole ALGORITHM, not merely the tail -- union,
39
+ * then sort, then append. two copies could drift step-for-step; one cannot.
40
+ * .note = `sortable` vs `ordered` is the domain's split for a declared line: inert
41
+ * position vs load-carrying position. see
42
+ * `.agent/repo=.this/role=any/briefs/domain.terms/term=ordered._.choice._.md`.
43
+ *
44
+ * ⚠️ .note = **THIS FUNCTION IS THE CONTRACT, NOT A CONVENIENCE.** every practice that declares
45
+ * the repo-root `.gitignore` converges only because all of them route through here. a
46
+ * declaration that hand-rolls its own union/sort/tail instead of this import is OUTSIDE
47
+ * that guarantee -- it may satisfy its own check and still oscillate against the rest,
48
+ * forever, on every `declapract fix`.
49
+ *
50
+ * that failure mode is measured, not hypothetical. every declaration of this file once
51
+ * carried its own copy of the algorithm, and one of them fell behind: it sorted every
52
+ * line and emitted no ordered tail, which hoists the `!` negations above their target and
53
+ * turns them inert. re-derive any declaration by hand today and the clamps reproduce it
54
+ * exactly -- that declaration's own idempotency assertions stay green while every
55
+ * cross-declarer assertion goes red.
56
+ *
57
+ * no build step stops the next author from a repeat. so: **if you declare the repo-root
58
+ * `.gitignore` from a new practice, import this -- do not re-derive it.** a lint rule or a
59
+ * single-owner registry would be the durable guard; today the cross-declarer clamps are.
60
+ */
61
+ export const defineExpectedGitignoreContents = (input: {
62
+ contents: string | null;
63
+ ignoresSortable: readonly string[];
64
+ }): string => {
65
+ // parse ignores from the repo's extant file
66
+ const ignoresFromFile = input.contents ? input.contents.split('\n') : [];
67
+
68
+ // separate the file's own lines into sortable vs ordered
69
+ const orderedPatterns = new Set(ignoresOrderedForGitignore);
70
+ const ignoresFromFileSortable = ignoresFromFile.filter(
71
+ (line) => !!line && !orderedPatterns.has(line),
72
+ );
73
+
74
+ // union the file's sortable lines with this practice's, then sort
75
+ const sortedIgnores = dedupe([
76
+ ...ignoresFromFileSortable,
77
+ ...input.ignoresSortable,
78
+ ])
79
+ .sort()
80
+ .filter((line) => !!line);
81
+
82
+ // append the ordered tail last, sequence preserved
83
+ return [...sortedIgnores, ...ignoresOrderedForGitignore, ''].join('\n');
84
+ };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "declapract-typescript-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "declapract best practices declarations for typescript",
5
- "version": "0.49.8",
5
+ "version": "0.49.9",
6
6
  "license": "MIT",
7
7
  "main": "src/index.js",
8
8
  "repository": "ehmpathy/declapract-typescript-ehmpathy",
@@ -76,15 +76,15 @@
76
76
  "esbuild-register": "3.6.0",
77
77
  "husky": "8.0.3",
78
78
  "jest": "30.2.0",
79
- "rhachet": "1.43.1",
80
- "rhachet-brains-anthropic": "0.4.1",
79
+ "rhachet": "1.44.4",
80
+ "rhachet-brains-anthropic": "0.4.3",
81
81
  "rhachet-brains-fireworksai": "0.1.3",
82
82
  "rhachet-brains-xai": "0.3.3",
83
- "rhachet-roles-bhrain": "0.30.2",
83
+ "rhachet-roles-bhrain": "0.31.3",
84
84
  "rhachet-roles-bhrowser": "0.1.0",
85
- "rhachet-roles-bhuild": "0.21.26",
86
- "rhachet-roles-ehmpathy": "1.38.1",
87
- "rhachet-roles-ghlitch": "0.2.5",
85
+ "rhachet-roles-bhuild": "0.21.31",
86
+ "rhachet-roles-ehmpathy": "1.38.6",
87
+ "rhachet-roles-ghlitch": "0.2.10",
88
88
  "rhachet-roles-rhachet": "0.1.7",
89
89
  "tsc-alias": "1.8.10",
90
90
  "tsx": "4.20.6",