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.
- package/dist/practices/cicd-app-react-native-expo/best-practice/.declapract.readme.md +27 -0
- package/dist/practices/cicd-app-react-native-expo/best-practice/.gitignore.declapract.ts +18 -11
- package/dist/practices/git/.declapract.integration.test.ts +950 -0
- package/dist/practices/git/.test/assets/repo-with-rhachet-only/declapract.use.yml +6 -0
- package/dist/practices/git/.test/assets/repo-with-rhachet-only/package.json +4 -0
- package/dist/practices/git/.test/assets/repo-without-cache-ignores/declapract.use.yml +9 -0
- package/dist/practices/git/.test/assets/repo-without-cache-ignores/package.json +4 -0
- package/dist/practices/git/__snapshots__/.declapract.integration.test.ts.snap +51 -0
- package/dist/practices/git/best-practice/.declapract.readme.md +23 -0
- package/dist/practices/git/best-practice/.gitignore.declapract.ts +59 -33
- package/dist/practices/rhachet/best-practice/.declapract.readme.md +32 -0
- package/dist/practices/rhachet/best-practice/.gitignore.declapract.ts +64 -0
- package/dist/utils/defineExpectedGitignoreContents.ts +84 -0
- package/package.json +7 -7
|
@@ -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,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,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
|
|
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
|
-
]
|
|
52
|
+
];
|
|
27
53
|
|
|
28
54
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
36
|
-
|
|
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.
|
|
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.
|
|
80
|
-
"rhachet-brains-anthropic": "0.4.
|
|
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.
|
|
83
|
+
"rhachet-roles-bhrain": "0.31.3",
|
|
84
84
|
"rhachet-roles-bhrowser": "0.1.0",
|
|
85
|
-
"rhachet-roles-bhuild": "0.21.
|
|
86
|
-
"rhachet-roles-ehmpathy": "1.38.
|
|
87
|
-
"rhachet-roles-ghlitch": "0.2.
|
|
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",
|