create-cmp-cli 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -11
- package/llms.txt +6 -2
- package/package.json +1 -1
- package/src/commands/upgrade.mjs +8 -1
- package/src/lib/adr-seed.mjs +178 -0
- package/src/lib/registry.mjs +15 -2
- package/src/lib/tabs.mjs +91 -4
- package/src/lib/upgrade.mjs +49 -5
- package/src/scaffold.mjs +52 -1
- package/src/versions/candidates.json +4 -0
- package/src/versions/registry.json +88 -0
- package/template/.claude/skills/add-feature/SKILL.md +35 -10
- package/template/.claude/skills/add-repository/SKILL.md +1 -1
- package/template/.claude/skills/add-screen/SKILL.md +13 -7
- package/template/.githooks/pre-push +24 -0
- package/template/CLAUDE.md +196 -48
- package/template/README.md +23 -27
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/CrashRecorder.kt +99 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/DbInspector.kt +144 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +69 -2
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +8 -4
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/NavInspector.kt +31 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/AppResultCatching.kt +32 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +9 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/DomainError.kt +21 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +4 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/result/AppResult.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +4 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +138 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +56 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppHeader.kt +54 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +16 -8
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentStateContainer.kt +105 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentUiState.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/EmptyState.kt +58 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ErrorState.kt +52 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ListItemCard.kt +77 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ScreenColumn.kt +47 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/Shimmer.kt +90 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +9 -9
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +5 -27
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +14 -70
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +33 -13
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +13 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +7 -109
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/NavInspectionHook.kt +21 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +4 -27
- package/template/composeApp/src/commonTest/kotlin/com/example/app/data/AppResultCatchingTest.kt +52 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +29 -4
- package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +8 -6
- package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +39 -27
- package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +10 -6
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/ComponentStories.kt +269 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt +37 -1
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +207 -15
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ComponentConformanceTest.kt +84 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +36 -4
- package/template/docs/ARCHITECTURE.md +317 -34
- package/template/docs/TESTING.md +6 -5
- package/template/docs/adr/0002-maestro-over-appium-for-e2e.md +39 -0
- package/template/docs/adr/0003-jvm-desktop-target-is-harness-infrastructure.md +39 -0
- package/template/docs/adr/0004-fakes-not-mocks-for-unit-tests.md +48 -0
- package/template/qa/approvals.json +42 -0
- package/template/qa/approve.mjs +139 -0
- package/template/qa/arch-doc.mjs +69 -0
- package/template/qa/comment.mjs +76 -0
- package/template/qa/comments.json +4 -0
- package/template/qa/golden/home.json +3 -3
- package/template/qa/lib/approvals.mjs +806 -0
- package/template/qa/lib/arch-doc.mjs +451 -0
- package/template/qa/lib/comments.mjs +252 -0
- package/template/qa/lib/component-stories.mjs +183 -0
- package/template/qa/lib/inputs-hash.mjs +5 -1
- package/template/qa/scaffold-feature.mjs +184 -67
- package/template/qa/setup-hooks.mjs +33 -0
- package/template/qa/verify.mjs +118 -9
- package/template/specs/app-base.spec.md +44 -7
- package/template/specs/home.spec.md +7 -4
- package/template/specs/intent.md +50 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ADR-0004: Fakes, not mocks, for unit tests
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** (scaffold date)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Every repository/source interface in this project's domain layer (`domain/repository/*.kt`)
|
|
9
|
+
needs a test double for ViewModel and use-case unit tests — see `FakeItemRepository`
|
|
10
|
+
(`composeApp/src/commonTest/.../testing/fakes/`), used by `HomeViewModelTest` and
|
|
11
|
+
`GetItemsUseCaseTest`. Mocking frameworks (MockK, Mockito) are the conventional default for
|
|
12
|
+
this in JVM-only Android projects, but this project is Kotlin Multiplatform: `commonTest` runs
|
|
13
|
+
on Android, iOS (native), and desktop (JVM) targets from one source set, and the bytecode-proxy
|
|
14
|
+
mechanism mocking frameworks rely on doesn't exist on Kotlin/Native. A mock-based test suite
|
|
15
|
+
would either be JVM-only (silently untested on iOS) or need a second, divergent native-only
|
|
16
|
+
test strategy.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
This project uses **hand-written fakes**, never mocking frameworks, for every domain-layer
|
|
21
|
+
interface under test. A fake lives in `testing/fakes/` (commonTest), implements the real
|
|
22
|
+
interface, and is configurable in the shape the exemplar establishes: a typed
|
|
23
|
+
`failure: DomainError?` to force the error path, seeded data for the success path, and
|
|
24
|
+
call-recording where a test needs to assert an interaction happened. A fake returns
|
|
25
|
+
`AppResult.Failure` when configured to fail — it never throws, because the domain contract it
|
|
26
|
+
implements (`domain/repository/*.kt`, ARCH-06) doesn't either.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
- One test double works identically on every target `commonTest` runs on — no JVM-only
|
|
31
|
+
behavior gap between what Android/desktop CI proves and what iOS actually ships.
|
|
32
|
+
- Fakes are slightly more code up front than a one-line `every { } returns` mock stub, but
|
|
33
|
+
they read as real (if simplified) implementations, which makes failure-path tests
|
|
34
|
+
(`failure = DomainError.Network`) as readable as success-path tests instead of relying on
|
|
35
|
+
stubbing syntax a reviewer has to decode.
|
|
36
|
+
- No mocking-framework dependency to keep on the frozen version set (`gradle/libs.versions.toml`)
|
|
37
|
+
or track for Kotlin/Native compatibility on every version bump.
|
|
38
|
+
- The cost: a new repository interface needs its fake hand-written before its consumers can be
|
|
39
|
+
tested — there is no "just mock it inline" escape hatch. This is intentional: an interface
|
|
40
|
+
too awkward to fake by hand is usually an interface with the wrong shape.
|
|
41
|
+
|
|
42
|
+
## Related
|
|
43
|
+
|
|
44
|
+
- `CLAUDE.md` — "Testing" section: "**Hand-written fakes** in `testing/fakes/` — never mocking
|
|
45
|
+
frameworks", the contract this ADR records the reasoning for.
|
|
46
|
+
- `docs/TESTING.md` — "Fakes, never mocks" under Unit conventions.
|
|
47
|
+
- `composeApp/src/commonTest/.../testing/fakes/FakeItemRepository.kt` — the exemplar fake this
|
|
48
|
+
ADR's shape is drawn from.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "cmp-approvals/1",
|
|
3
|
+
"exemplarFeature": "home",
|
|
4
|
+
"artifacts": [
|
|
5
|
+
{
|
|
6
|
+
"artifact": "intent",
|
|
7
|
+
"status": "unreviewed",
|
|
8
|
+
"hash": null,
|
|
9
|
+
"approvedAt": null
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"artifact": "design-system",
|
|
13
|
+
"status": "unreviewed",
|
|
14
|
+
"hash": null,
|
|
15
|
+
"approvedAt": null
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"artifact": "architecture",
|
|
19
|
+
"status": "unreviewed",
|
|
20
|
+
"hash": null,
|
|
21
|
+
"approvedAt": null
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"artifact": "components",
|
|
25
|
+
"status": "unreviewed",
|
|
26
|
+
"hash": null,
|
|
27
|
+
"approvedAt": null
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"artifact": "exemplar-feature",
|
|
31
|
+
"status": "unreviewed",
|
|
32
|
+
"hash": null,
|
|
33
|
+
"approvedAt": null
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"artifact": "exemplar-spec",
|
|
37
|
+
"status": "unreviewed",
|
|
38
|
+
"hash": null,
|
|
39
|
+
"approvedAt": null
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The approvals CLI — thin shell over qa/lib/approvals.mjs.
|
|
3
|
+
//
|
|
4
|
+
// node qa/approve.mjs <artifact> records approval (recomputes the artifact's
|
|
5
|
+
// hash now, stamps the time, writes qa/approvals.json)
|
|
6
|
+
// node qa/approve.mjs --status lists every governed artifact + live state
|
|
7
|
+
// (unreviewed / approved / changed-since-approval /
|
|
8
|
+
// reopened, + mode when set) + short hash
|
|
9
|
+
// node qa/approve.mjs --accept-defaults express lane (GENESIS-FLOW-DESIGN.md §2): approves
|
|
10
|
+
// every currently-resolvable, not-yet-approved
|
|
11
|
+
// artifact, each stamped "defaults-accepted"
|
|
12
|
+
// node qa/approve.mjs --reopen <artifact> moves an approved artifact back to "reopened" for
|
|
13
|
+
// redesign (refuses anything not currently approved)
|
|
14
|
+
//
|
|
15
|
+
// This file has NO logic of its own — every decision (the registry, hashing,
|
|
16
|
+
// state, the transitions) lives in qa/lib/approvals.mjs. That's deliberate: the
|
|
17
|
+
// console (VERIFICATION-LAYER-DESIGN.md §4, `POST /api/approve`; GENESIS-FLOW-DESIGN.md
|
|
18
|
+
// §2, `POST /api/reopen`) calls the SAME library this CLI calls, so this file is the
|
|
19
|
+
// API surface, kept intentionally thin and easy to keep in lockstep.
|
|
20
|
+
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
approveAllDefaults,
|
|
26
|
+
approveArtifact,
|
|
27
|
+
getApprovalStatuses,
|
|
28
|
+
isPackageResolvable,
|
|
29
|
+
listGovernedArtifacts,
|
|
30
|
+
reopenArtifact,
|
|
31
|
+
} from "./lib/approvals.mjs";
|
|
32
|
+
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
function shortHash(hash) {
|
|
37
|
+
return hash ? hash.slice(0, 8) : "none";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printStatus() {
|
|
41
|
+
const statuses = getApprovalStatuses(ROOT);
|
|
42
|
+
if (statuses.length === 0) {
|
|
43
|
+
console.log("No governed artifacts resolved in this project (no specs/, or the package could not be resolved).");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log("Approval status:\n");
|
|
47
|
+
for (const s of statuses) {
|
|
48
|
+
const mark =
|
|
49
|
+
s.status === "approved" ? "✓" : s.status === "changed-since-approval" ? "✗" : s.status === "reopened" ? "↺" : "→";
|
|
50
|
+
// An unresolvable artifact (0 files, or a partial kotlin set in a
|
|
51
|
+
// pre-stamp tree) must never display its degraded hash as if it were
|
|
52
|
+
// approvable — approval over an unresolved file set is refused.
|
|
53
|
+
const hashInfo =
|
|
54
|
+
s.status === "reopened"
|
|
55
|
+
? `reopened at ${s.reopenedAt} (was approved ${shortHash(s.storedHash)})`
|
|
56
|
+
: s.status === "changed-since-approval"
|
|
57
|
+
? s.resolvable
|
|
58
|
+
? `approved ${shortHash(s.storedHash)} -> now ${shortHash(s.hash)}`
|
|
59
|
+
: `approved ${shortHash(s.storedHash)} -> unresolvable (${s.fileCount} of expected files resolved)`
|
|
60
|
+
: s.status === "approved"
|
|
61
|
+
? shortHash(s.hash)
|
|
62
|
+
: s.resolvable
|
|
63
|
+
? `would approve at ${shortHash(s.hash)}`
|
|
64
|
+
: `unresolvable (${s.fileCount} of expected files resolved) — not approvable`;
|
|
65
|
+
const modeInfo = s.mode ? ` [${s.mode}]` : "";
|
|
66
|
+
console.log(`${mark} ${s.id}: ${s.status} (${hashInfo})${modeInfo} — ${s.label}`);
|
|
67
|
+
if (s.missing.length > 0) {
|
|
68
|
+
console.log(` missing: ${s.missing.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (args.includes("--status")) {
|
|
74
|
+
printStatus();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Write guard: refuse to RECORD approvals in a tree whose package is not
|
|
79
|
+
// resolvable (the raw template / a pre-stamp tree). Approvals belong to a
|
|
80
|
+
// generated project; writing qa/approvals.json into the template pollutes the
|
|
81
|
+
// template itself. Read-only --status (above) stays available anywhere. Applies
|
|
82
|
+
// to every write operation below (single approve, express lane, reopen).
|
|
83
|
+
function refuseIfUnresolvable() {
|
|
84
|
+
if (isPackageResolvable(ROOT)) return;
|
|
85
|
+
console.error(
|
|
86
|
+
"error: this tree's package is not resolvable (composeApp/build.gradle.kts namespace is missing or still a placeholder) — " +
|
|
87
|
+
"this looks like the raw template or a pre-stamp tree. Approvals are recorded in a generated project; refusing to write qa/approvals.json here.",
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (args.includes("--accept-defaults")) {
|
|
93
|
+
refuseIfUnresolvable();
|
|
94
|
+
const { approved, skipped } = approveAllDefaults(ROOT);
|
|
95
|
+
for (const id of approved) {
|
|
96
|
+
console.log(`✓ approved ${id} [defaults-accepted]`);
|
|
97
|
+
}
|
|
98
|
+
for (const s of skipped) {
|
|
99
|
+
console.log(`→ skipped ${s.id}: ${s.reason}`);
|
|
100
|
+
}
|
|
101
|
+
console.log(`\n${approved.length} approved (defaults-accepted), ${skipped.length} skipped (unresolvable).`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reopenFlagIdx = args.indexOf("--reopen");
|
|
106
|
+
if (reopenFlagIdx !== -1) {
|
|
107
|
+
refuseIfUnresolvable();
|
|
108
|
+
const artifactId = args[reopenFlagIdx + 1];
|
|
109
|
+
if (!artifactId) {
|
|
110
|
+
console.error("usage: node qa/approve.mjs --reopen <artifact>");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const result = reopenArtifact(ROOT, artifactId);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
console.error(`error: ${result.reason}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
console.log(`↺ reopened ${result.artifact} for redesign — at ${result.reopenedAt}`);
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (args.length === 0) {
|
|
123
|
+
const ids = listGovernedArtifacts(ROOT).map((a) => a.id);
|
|
124
|
+
console.error(
|
|
125
|
+
"usage: node qa/approve.mjs <artifact> | --status | --accept-defaults | --reopen <artifact>\n" +
|
|
126
|
+
` valid artifacts: ${ids.length > 0 ? ids.join(", ") : "(none resolved in this project)"}`,
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
refuseIfUnresolvable();
|
|
132
|
+
|
|
133
|
+
const artifactId = args[0];
|
|
134
|
+
const result = approveArtifact(ROOT, artifactId);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
console.error(`error: ${result.reason}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The architecture-doc generator/checker — thin shell over qa/lib/arch-doc.mjs,
|
|
3
|
+
// mirroring qa/approve.mjs's CLI-over-library split.
|
|
4
|
+
//
|
|
5
|
+
// node qa/arch-doc.mjs regenerate docs/ARCHITECTURE.md's
|
|
6
|
+
// `cmp:generated` sections in place from a
|
|
7
|
+
// real tree walk — touches NOTHING outside
|
|
8
|
+
// the markers
|
|
9
|
+
// node qa/arch-doc.mjs --check exit nonzero (naming every stale/missing
|
|
10
|
+
// section) if regenerating would change the
|
|
11
|
+
// file; never writes
|
|
12
|
+
//
|
|
13
|
+
// This file has no logic of its own — every decision (what each section
|
|
14
|
+
// derives from, the marker grammar) lives in qa/lib/arch-doc.mjs. The verify
|
|
15
|
+
// lane's `archDoc` step (qa/verify.mjs) calls the SAME library in --check mode.
|
|
16
|
+
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
import { ARCH_DOC_REL_PATH, regenerateArchDoc, writeArchDoc } from "./lib/arch-doc.mjs";
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const checkOnly = process.argv.includes("--check");
|
|
24
|
+
|
|
25
|
+
function reportUnknown(result) {
|
|
26
|
+
if (result.unknownSections.length === 0) return false;
|
|
27
|
+
console.error(
|
|
28
|
+
`error: ${ARCH_DOC_REL_PATH} has cmp:generated marker(s) with no registered generator: ${result.unknownSections.join(", ")}`,
|
|
29
|
+
);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (checkOnly) {
|
|
34
|
+
const result = regenerateArchDoc(ROOT);
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
console.error(`error: ${result.reason}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (reportUnknown(result)) process.exit(1);
|
|
40
|
+
|
|
41
|
+
const stale = result.changed || result.missingSections.length > 0;
|
|
42
|
+
if (!stale) {
|
|
43
|
+
console.log(`✓ ${ARCH_DOC_REL_PATH} is fresh — every generated section matches the tree.`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.error(`✗ ${ARCH_DOC_REL_PATH} is stale:`);
|
|
48
|
+
for (const id of result.changedSections) {
|
|
49
|
+
console.error(` [${id}] regenerating would change this section — the tree no longer matches the doc`);
|
|
50
|
+
}
|
|
51
|
+
for (const id of result.missingSections) {
|
|
52
|
+
console.error(` [${id}] marker missing from the doc entirely — never generated`);
|
|
53
|
+
}
|
|
54
|
+
console.error("Run: node qa/arch-doc.mjs");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = writeArchDoc(ROOT);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
console.error(`error: ${result.reason}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (reportUnknown(result)) process.exit(1);
|
|
64
|
+
|
|
65
|
+
if (!result.wrote) {
|
|
66
|
+
console.log(`✓ ${ARCH_DOC_REL_PATH} already fresh — nothing to regenerate.`);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
console.log(`✓ regenerated ${ARCH_DOC_REL_PATH} — updated section(s): ${result.changedSections.join(", ")}`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The comments CLI — thin shell over qa/lib/comments.mjs (mirrors qa/approve.mjs).
|
|
3
|
+
//
|
|
4
|
+
// node qa/comment.mjs --list [--open] readable table of the ledger,
|
|
5
|
+
// including resolution notes
|
|
6
|
+
// node qa/comment.mjs --resolve <id> --note "..." resolves a comment as "agent-cli",
|
|
7
|
+
// recording the note explaining
|
|
8
|
+
// what changed as a result
|
|
9
|
+
//
|
|
10
|
+
// This file has NO logic of its own — every decision (validation, ids, the ledger)
|
|
11
|
+
// lives in qa/lib/comments.mjs. Adding a comment is a console/human action (the
|
|
12
|
+
// console's `POST /api/comment` calls the same library through a bridge); this CLI
|
|
13
|
+
// covers the agent's side of the loop of record: observe, act, resolve.
|
|
14
|
+
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
import { listComments, resolveComment } from "./lib/comments.mjs";
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
function argValue(flag) {
|
|
24
|
+
const idx = args.indexOf(flag);
|
|
25
|
+
if (idx === -1 || idx === args.length - 1) return undefined;
|
|
26
|
+
return args[idx + 1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printList(openOnly) {
|
|
30
|
+
let result;
|
|
31
|
+
try {
|
|
32
|
+
result = listComments(ROOT, openOnly ? { status: "open" } : {});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`error: ${err.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (result.comments.length === 0) {
|
|
38
|
+
console.log(openOnly ? "No open comments." : "No comments recorded yet.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`Comments${openOnly ? " (open only)" : ""}:\n`);
|
|
42
|
+
for (const c of result.comments) {
|
|
43
|
+
const mark = c.status === "resolved" ? "✓" : "→";
|
|
44
|
+
console.log(`${mark} ${c.id} [${c.target?.type ?? "unknown"}] ${c.status} — by ${c.author} at ${c.createdAt}`);
|
|
45
|
+
console.log(` ${c.text}`);
|
|
46
|
+
if (c.status === "resolved") {
|
|
47
|
+
const note = c.resolutionNote ? `: ${c.resolutionNote}` : " (no note)";
|
|
48
|
+
console.log(` resolved by ${c.resolvedBy} at ${c.resolvedAt}${note}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (args.includes("--list")) {
|
|
54
|
+
printList(args.includes("--open"));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (args.includes("--resolve")) {
|
|
59
|
+
const id = argValue("--resolve");
|
|
60
|
+
if (!id) {
|
|
61
|
+
console.error('usage: node qa/comment.mjs --resolve <id> --note "..."');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const note = argValue("--note");
|
|
65
|
+
const result = resolveComment(ROOT, id, { note, author: "agent-cli" });
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
console.error(`error: ${result.reason}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const noteSuffix = result.comment.resolutionNote ? ` — ${result.comment.resolutionNote}` : "";
|
|
71
|
+
console.log(`✓ resolved ${result.comment.id}${noteSuffix}`);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.error('usage: node qa/comment.mjs --list [--open] | --resolve <id> --note "..."');
|
|
76
|
+
process.exit(1);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{"children": [
|
|
2
|
-
{"children": [
|
|
2
|
+
{"tag": "home_screen", "children": [
|
|
3
3
|
{"tag": "home_title", "text": "Home"},
|
|
4
4
|
{"children": [
|
|
5
|
-
{"children": [
|
|
5
|
+
{"tag": "home_item_1", "children": [
|
|
6
6
|
{"text": "Golden first"},
|
|
7
7
|
{"text": "Structural baseline row one"}
|
|
8
8
|
]},
|
|
9
|
-
{"children": [
|
|
9
|
+
{"tag": "home_item_2", "children": [
|
|
10
10
|
{"text": "Golden second"},
|
|
11
11
|
{"text": "Structural baseline row two"}
|
|
12
12
|
]}
|