create-cmp-cli 0.23.0 → 0.25.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.
Files changed (145) hide show
  1. package/bin/create-cmp.mjs +33 -3
  2. package/package.json +10 -4
  3. package/packages/harness/package.json +18 -8
  4. package/packages/harness/src/approve.mjs +46 -18
  5. package/packages/harness/src/console/console-data.mjs +117 -0
  6. package/packages/harness/src/console/console-evidence.mjs +122 -0
  7. package/packages/harness/src/console/console-overview.mjs +642 -0
  8. package/packages/harness/src/console/console-shell.mjs +1139 -0
  9. package/packages/harness/src/console/console-tabs.mjs +2828 -0
  10. package/packages/harness/src/console/contrast.mjs +74 -0
  11. package/packages/harness/src/console/preview-service.mjs +1384 -0
  12. package/packages/harness/src/framework-check.mjs +747 -0
  13. package/packages/harness/src/lib/a11y.mjs +1 -1
  14. package/packages/harness/src/lib/affected-tests.mjs +99 -44
  15. package/packages/harness/src/lib/agent-hold.mjs +234 -0
  16. package/packages/harness/src/lib/approvals.mjs +177 -350
  17. package/packages/harness/src/lib/audit-cadence.mjs +1 -1
  18. package/packages/harness/src/lib/comments.mjs +18 -4
  19. package/packages/harness/src/lib/determinism.mjs +275 -14
  20. package/packages/harness/src/lib/evidence-badge.mjs +38 -6
  21. package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
  22. package/packages/harness/src/lib/evidence-level.mjs +144 -59
  23. package/packages/harness/src/lib/feature-brief.mjs +53 -16
  24. package/packages/harness/src/lib/flight-recorder.mjs +72 -8
  25. package/packages/harness/src/lib/framework-check.mjs +778 -0
  26. package/packages/harness/src/lib/harness-lock.mjs +87 -19
  27. package/packages/harness/src/lib/harness-manifest.mjs +208 -0
  28. package/packages/harness/src/lib/harness-region.mjs +106 -2
  29. package/packages/harness/src/lib/harness-source.mjs +142 -0
  30. package/packages/harness/src/lib/inputs-hash.mjs +180 -22
  31. package/packages/harness/src/lib/lane-markers.mjs +78 -0
  32. package/packages/harness/src/lib/lane-narrator.mjs +4 -2
  33. package/packages/harness/src/lib/lane-runner.mjs +72 -6
  34. package/packages/harness/src/lib/plan.mjs +44 -11
  35. package/packages/harness/src/lib/plant-calibration.mjs +135 -0
  36. package/packages/harness/src/lib/profile-loader.mjs +323 -0
  37. package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
  38. package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
  39. package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
  40. package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  41. package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
  42. package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
  43. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  44. package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
  45. package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
  46. package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
  47. package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
  48. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
  49. package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  50. package/packages/harness/src/lib/receipt-validate.mjs +84 -16
  51. package/packages/harness/src/lib/spec-coverage.mjs +179 -92
  52. package/packages/harness/src/lib/spec-model.mjs +262 -0
  53. package/packages/harness/src/lib/step-cache.mjs +11 -2
  54. package/packages/harness/src/lib/step-outcomes.mjs +84 -151
  55. package/packages/harness/src/lib/walk.mjs +1 -1
  56. package/packages/harness/src/plan.mjs +30 -1
  57. package/packages/harness/src/preview-gallery.mjs +1 -1
  58. package/packages/harness/src/receipt-check.mjs +95 -26
  59. package/packages/harness/src/record-audit.mjs +1 -1
  60. package/packages/harness/src/scaffold-feature.mjs +10 -2
  61. package/packages/harness/src/verify.mjs +222 -142
  62. package/packages/harness/src/watch.mjs +146 -33
  63. package/packages/receipts/package.json +1 -1
  64. package/packages/receipts/src/index.mjs +2 -2
  65. package/packages/receipts/src/inputs-hash.mjs +180 -22
  66. package/packages/receipts/src/receipt-validate.mjs +84 -16
  67. package/src/commands/attach.mjs +128 -4
  68. package/src/commands/harden.mjs +7 -1
  69. package/src/commands/upgrade.mjs +7 -0
  70. package/src/lib/minimal.mjs +23 -1
  71. package/src/scaffold.mjs +8 -0
  72. package/template/.claude/settings.json +2 -2
  73. package/template/.claude/skills/add-feature/SKILL.md +7 -3
  74. package/template/.claude/skills/add-repository/SKILL.md +2 -2
  75. package/template/.claude/skills/add-screen/SKILL.md +8 -4
  76. package/template/.githooks/pre-push +9 -0
  77. package/template/CLAUDE.md +55 -186
  78. package/template/docs/TESTING.md +1 -1
  79. package/template/gitignore +4 -0
  80. package/template/qa/approve.mjs +46 -18
  81. package/template/qa/comments.json +1 -1
  82. package/template/qa/evidence/schema.json +258 -34
  83. package/template/qa/framework-check.mjs +747 -0
  84. package/template/qa/harness-manifest.json +10 -0
  85. package/template/qa/lib/a11y.mjs +1 -1
  86. package/template/qa/lib/affected-tests.mjs +99 -44
  87. package/template/qa/lib/agent-hold.mjs +234 -0
  88. package/template/qa/lib/approvals.mjs +177 -350
  89. package/template/qa/lib/audit-cadence.mjs +1 -1
  90. package/template/qa/lib/comments.mjs +18 -4
  91. package/template/qa/lib/determinism.mjs +275 -14
  92. package/template/qa/lib/evidence-badge.mjs +38 -6
  93. package/template/qa/lib/evidence-ladder.mjs +202 -0
  94. package/template/qa/lib/evidence-level.mjs +144 -59
  95. package/template/qa/lib/feature-brief.mjs +53 -16
  96. package/template/qa/lib/flight-recorder.mjs +72 -8
  97. package/template/qa/lib/framework-check.mjs +778 -0
  98. package/template/qa/lib/harness-lock.mjs +87 -19
  99. package/template/qa/lib/harness-manifest.mjs +208 -0
  100. package/template/qa/lib/harness-region.mjs +106 -2
  101. package/template/qa/lib/harness-source.mjs +142 -0
  102. package/template/qa/lib/inputs-hash.mjs +180 -22
  103. package/template/qa/lib/lane-markers.mjs +78 -0
  104. package/template/qa/lib/lane-narrator.mjs +4 -2
  105. package/template/qa/lib/lane-runner.mjs +72 -6
  106. package/template/qa/lib/plan.mjs +44 -11
  107. package/template/qa/lib/plant-calibration.mjs +135 -0
  108. package/template/qa/lib/profile-loader.mjs +323 -0
  109. package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
  110. package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
  111. package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
  112. package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  113. package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
  114. package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
  115. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  116. package/template/qa/lib/profiles/cmp/index.mjs +47 -0
  117. package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
  118. package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
  119. package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
  120. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
  121. package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  122. package/template/qa/lib/receipt-validate.mjs +84 -16
  123. package/template/qa/lib/spec-coverage.mjs +179 -92
  124. package/template/qa/lib/spec-model.mjs +262 -0
  125. package/template/qa/lib/step-cache.mjs +11 -2
  126. package/template/qa/lib/step-outcomes.mjs +84 -151
  127. package/template/qa/lib/walk.mjs +1 -1
  128. package/template/qa/plan.mjs +30 -1
  129. package/template/qa/preview-gallery.mjs +1 -1
  130. package/template/qa/receipt-check.mjs +95 -26
  131. package/template/qa/record-audit.mjs +1 -1
  132. package/template/qa/scaffold-feature.mjs +10 -2
  133. package/template/qa/verified-surface.json +17 -0
  134. package/template/qa/verify.mjs +222 -142
  135. package/template/qa/watch.mjs +146 -33
  136. /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  137. /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  138. /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  139. /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  140. /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
  141. /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  142. /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  143. /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  144. /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  145. /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
@@ -10,19 +10,14 @@
10
10
  //
11
11
  // Three concerns, kept separable:
12
12
  // 1. The REGISTRY (`listGovernedArtifacts`) — artifact id -> resolved file list, in
13
- // definition order (GENESIS-FLOW-DESIGN.md §1 + CHANGE-FLOW-DESIGN.md §6):
14
- // intent(0), feature-brief:<name> per docs/features/*.md (the decide layer,
15
- // directly after intent), architecture, exemplar-spec, exemplar-feature,
16
- // design-system, components, then one `feature-spec:<name>` per non-base,
17
- // non-exemplar spec file present in specs/ right now. (Decide-first: a brief
18
- // speaks intent's vocabulary. Spec-first: the exemplar's clauses are confirmed
19
- // before the slice is built. UI-first: design system + components are distilled
20
- // from the real screens, so they lock after the exemplar.) The exemplar is
21
- // CONFIGURABLE — see
22
- // `getExemplarFeature`/`resolveExemplarNames` below — defaulting to `home` so
23
- // every ledger written before this config key existed keeps meaning what it
24
- // meant. The registry is recomputed on every call — it reflects the tree as it
25
- // stands, never a stale snapshot.
13
+ // definition order. WHICH artifacts, in what order, and how each is hashed is
14
+ // the PROFILE's (Stage 0 PR 5): the profile the manifest names exports
15
+ // `artifacts(root)`, composing its own entries with the neutral helpers below
16
+ // (feature briefs, feature designs, feature specs, the architecture artifact).
17
+ // Mobile's list intent, briefs, architecture, exemplar-spec, exemplar-feature,
18
+ // design-system, components, designs, feature specs lives in
19
+ // profiles/cmp/artifacts.mjs. The registry is recomputed on every call it
20
+ // reflects the tree as it stands, never a stale snapshot.
26
21
  // 2. STATE (`loadApprovals`/`saveApprovals`) — qa/approvals.json, the human's
27
22
  // decisions: { artifact, status, hash, approvedAt, mode?, reopenedAt?, via?,
28
23
  // reason? } plus the top-level `exemplarFeature` config key. Absent or corrupt is TOLERATED
@@ -51,18 +46,23 @@
51
46
  //
52
47
  // Consumers: qa/approve.mjs (the CLI — thin shell over this file), qa/verify.mjs
53
48
  // (the `approvals` gate), qa/scaffold-feature.mjs (seeds a new feature's spec as
54
- // unreviewed, and resolves its clone-FROM exemplar through `resolveExemplarNames`).
49
+ // unreviewed; its exemplar names come from the cmp profile).
55
50
  // The console (inspector/mcp/src/lib/approvals-bridge.mjs) calls this same library.
56
51
 
57
52
  import { createHash } from "node:crypto";
58
53
  import fs from "node:fs";
59
54
  import path from "node:path";
60
55
 
61
- import { ARCH_DOC_REL_PATH, stripGeneratedSections } from "./arch-doc.mjs";
56
+ import { stripGeneratedSections } from "./arch-doc.mjs";
62
57
  import { deriveAllFeatures, deriveFeatureStatus, listFeatureBriefs, parseFeatureBlock, stripFeatureBlock } from "./feature-brief.mjs";
58
+ import { resolveHarnessManifest } from "./harness-manifest.mjs";
59
+ import { loadProfileSync } from "./profile-loader.mjs";
63
60
 
64
61
  export const APPROVALS_REL_PATH = "qa/approvals.json";
65
- export const APPROVALS_SCHEMA = "cmp-approvals/1";
62
+ // Free to move: the reader is tolerant by construction — `parsed.schema ??
63
+ // APPROVALS_SCHEMA` — so a ledger written under the old name reads back
64
+ // unchanged and is rewritten under the new one at the next signature.
65
+ export const APPROVALS_SCHEMA = "prooflane-approvals/1";
66
66
  // The JOURNAL (2026-07-28 flow audit, fix 1): qa/approvals.json is a mutable
67
67
  // snapshot — every transition overwrites the row, so the ledger cannot answer
68
68
  // "what happened while I was away?". The journal is the append-only memory
@@ -74,212 +74,160 @@ export const APPROVALS_SCHEMA = "cmp-approvals/1";
74
74
  // invalidate the receipt for a tree whose code did not change.
75
75
  export const APPROVALS_JOURNAL_REL_PATH = "qa/approvals.log.jsonl";
76
76
 
77
- // Kotlin source-set roots, relative to project root — mirrors qa/scaffold-feature.mjs's
78
- // SRC() helper (composeApp/src/<sourceSet>/kotlin/<packageDir>).
79
- const KOTLIN_SOURCE_SETS = {
80
- commonMain: "composeApp/src/commonMain/kotlin",
81
- commonTest: "composeApp/src/commonTest/kotlin",
82
- desktopTest: "composeApp/src/desktopTest/kotlin",
83
- };
77
+ // ── The profile ─────────────────────────────────────────────────────────────
78
+ // WHICH artifacts a human signs, in what order, and how each is hashed is the
79
+ // profile's model (Stage 0 PR 5 — NORTH-STAR §6; profiles/cmp/artifacts.mjs
80
+ // holds mobile's six genesis artifacts). This file keeps the MECHANIC: an
81
+ // artifact is a path set with a hash; a signature binds the hash; status is
82
+ // derived on every read; reopen and accept are attributed; the gate reads it
83
+ // all. The neutral entries every profile shares — feature briefs (Decide),
84
+ // feature designs (one per brief with a surface) and feature specs — are
85
+ // built by the helpers below, which a profile's `artifacts(root)` composes.
84
86
 
85
- // The canonical 11-file EXEMPLAR SHAPE (10 kotlin files + 1 spec), parametrized by
86
- // the exemplar's own names — F (PascalCase feature, e.g. "Home"), f (lowercase
87
- // package segment, e.g. "home"), E (PascalCase entity, e.g. "Item"). This is the
88
- // SAME shape qa/scaffold-feature.mjs's ALL_FILES clones FROM (GENESIS-FLOW-DESIGN.md
89
- // §1's "configurable exemplar") — the stamper imports this exact function so the
90
- // clone-source list and the governed-artifact list can never drift from each other
91
- // (single source of truth, not a parallel copy to keep in sync by hand).
92
- // @param {string} F PascalCase feature name (e.g. "Home", "Favorites")
93
- // @param {string} f lowercase package-segment name (e.g. "home", "favorites")
94
- // @param {string} E PascalCase entity name (e.g. "Item", "Favorite")
95
- // @returns {Array<{sourceSet: string, rel: string}>}
96
- export function exemplarKotlinFileSet(F, f, E) {
97
- return [
98
- { sourceSet: "commonMain", rel: `domain/model/${E}.kt` },
99
- { sourceSet: "commonMain", rel: `domain/repository/${E}Repository.kt` },
100
- { sourceSet: "commonMain", rel: `domain/usecase/Get${E}sUseCase.kt` },
101
- { sourceSet: "commonMain", rel: `data/remote/${E}RepositoryImpl.kt` },
102
- { sourceSet: "commonTest", rel: `testing/fakes/Fake${E}Repository.kt` },
103
- { sourceSet: "commonMain", rel: `presentation/${f}/${F}Screen.kt` },
104
- { sourceSet: "commonMain", rel: `presentation/${f}/${F}ViewModel.kt` },
105
- { sourceSet: "commonTest", rel: `presentation/${f}/${F}ViewModelTest.kt` },
106
- { sourceSet: "desktopTest", rel: `presentation/${f}/${F}ScreenTest.kt` },
107
- { sourceSet: "desktopTest", rel: `presentation/${f}/${F}GoldenTreeTest.kt` },
108
- ];
109
- }
110
-
111
- // Naive de-pluralization, shared verbatim with qa/scaffold-feature.mjs's own
112
- // entity-name default (a feature stamped without `--entity` gets this exact
113
- // guess). Exported so both the stamper (deriving a NEW feature's entity) and this
114
- // registry (guessing a CONFIGURED exemplar's entity from its feature name alone —
115
- // see resolveExemplarNames) apply the identical heuristic. Unreliable for
116
- // irregular nouns by design (the skill surfaces the guess for human override at
117
- // stamp time); a wrong guess here simply fails to resolve files, which is refused
118
- // (never fabricated), not silently wrong.
119
- export function defaultEntityName(feature) {
120
- if (feature.endsWith("ies") && feature.length > 3) return `${feature.slice(0, -3)}y`;
121
- if (feature.endsWith("s") && !feature.endsWith("ss")) return feature.slice(0, -1);
122
- return feature;
123
- }
124
-
125
- function toPascalCase(f) {
126
- return f.charAt(0).toUpperCase() + f.slice(1);
127
- }
128
-
129
- function toUpperSnake(F) {
130
- return F.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
131
- }
87
+ /** The `feature-design:` id prefix one place, so the CLI/console/gate never drift on it. */
88
+ export const FEATURE_DESIGN_PREFIX = "feature-design:";
132
89
 
133
90
  /**
134
- * Resolve the CONFIGURED exemplar's names — the ones the exemplar-feature/
135
- * exemplar-spec governed artifacts (and qa/scaffold-feature.mjs's clone source)
136
- * are built from.
137
- *
138
- * `home` (the default, and the only exemplar that predates configurability) is a
139
- * hardcoded exception: its entity is `Item`, not derivable from `Home` by
140
- * `defaultEntityName` (which would naively guess `Home`). Every OTHER exemplar is
141
- * itself a feature that was stamped by qa/scaffold-feature.mjs, so its entity
142
- * followed defaultEntityName(F) UNLESS it was stamped with an explicit `--entity`
143
- * override — a choice this config key cannot see. In that mismatch case the guess
144
- * is wrong and the file set simply fails to resolve (0 or partial files), which
145
- * `resolveArtifactStatus`/`approveArtifact` already refuse rather than fabricate —
146
- * the correct failure mode, not a special case to add here.
91
+ * The profile the manifest names, loaded synchronously — the registry is read
92
+ * from sync chains (the gate, the CLI, the console bridge). No manifest or no
93
+ * profile is a refusal by name, never an empty registry: an empty ledger that
94
+ * looked governed would be the vacuous PASS this file exists to refuse.
147
95
  * @param {string} root
148
- * @returns {{f: string, F: string, F_UPPER: string, E: string}}
96
+ * @returns {object} the profile module
149
97
  */
150
- export function resolveExemplarNames(root) {
151
- const f = getExemplarFeature(root);
152
- const F = toPascalCase(f);
153
- const F_UPPER = toUpperSnake(F);
154
- const E = f === "home" ? "Item" : defaultEntityName(F);
155
- return { f, F, F_UPPER, E };
98
+ function profileFor(root) {
99
+ const manifest = resolveHarnessManifest(root);
100
+ if (!manifest.ok) throw new Error(manifest.reason);
101
+ const loaded = loadProfileSync(root, manifest.manifest.profile);
102
+ if (!loaded.ok) throw new Error(loaded.reason);
103
+ return loaded.profile;
156
104
  }
157
105
 
158
- // Backward-compatible constants for the DEFAULT (`home`) exemplar — kept exported
159
- // because they describe the shipped template's own exemplar shape independent of
160
- // any project's configuration, and because they're the fixture the "stamping from
161
- // home must be byte-identical" pin (test/genesis-flow.test.mjs) anchors to.
162
- export const EXEMPLAR_FEATURE_KOTLIN_FILES = exemplarKotlinFileSet("Home", "home", "Item");
163
- export const EXEMPLAR_SPEC_REL = "specs/home.spec.md";
164
- export const ARCHITECTURE_SPEC_REL = "specs/app-base.spec.md";
165
- export const INTENT_REL = "specs/intent.md";
166
-
167
- // ── Package resolution ───────────────────────────────────────────────────────
168
- // Mirrors qa/scaffold-feature.mjs's resolvePackage() primary path (the
169
- // composeApp/build.gradle.kts namespace). Unlike the stamper, this NEVER dies —
170
- // an unresolved package means the kotlin-rooted artifacts resolve to zero files.
171
- // Zero resolution never CRASHES anything (the lane and the stamper stay up),
172
- // but it is NOT benign for decisions: an approval over zero files would be the
173
- // empty-input sha256 attesting nothing — a silent vacuous PASS, the exact
174
- // failure mode this harness exists to kill (evidence must attest execution).
175
- // So: approveArtifact REFUSES zero-file artifacts, and an already-approved
176
- // artifact whose files stop resolving goes to changed-since-approval (FAIL),
177
- // never PASS.
178
- //
179
- // IMPORTANT: detect "unresolved" by TOKEN SHAPE (`/^__[A-Z_]+__$/`), never by
180
- // comparing against the literal string "__PACKAGE__". This file ships through
181
- // the SAME scaffold pipeline that resolves that token — a literal comparison
182
- // string is itself blindly text-substituted at stamp time (`replaceContents`
183
- // does a global `"__PACKAGE__" -> config.package` replace over every template
184
- // file's content, this one included), which would silently rewrite the
185
- // sentinel into the real package and make the check always fail. A shape
186
- // regex never spells the token out, so the pipeline has nothing to match.
187
- const UNRESOLVED_TOKEN_RE = /^__[A-Z_]+__$/;
188
-
189
- function resolvePackageDir(root) {
190
- const gradleFile = path.join(root, "composeApp", "build.gradle.kts");
191
- if (!fs.existsSync(gradleFile)) return null;
192
- let contents;
106
+ /**
107
+ * May approvals be RECORDED in this tree? The core requires a manifest and a
108
+ * loadable profile; the profile may add its own test (`governable(root)` —
109
+ * mobile refuses the raw template, whose package is still a placeholder).
110
+ * Read-only status stays available anywhere.
111
+ * @param {string} root
112
+ * @returns {{ok: true} | {ok: false, reason: string}}
113
+ */
114
+ export function isProjectGovernable(root) {
115
+ let profile;
193
116
  try {
194
- contents = fs.readFileSync(gradleFile, "utf8");
195
- } catch {
196
- return null;
117
+ profile = profileFor(root);
118
+ } catch (err) {
119
+ return { ok: false, reason: err && err.message ? err.message : String(err) };
197
120
  }
198
- const m = contents.match(/namespace\s*=\s*"([^"]+)"/);
199
- if (!m || UNRESOLVED_TOKEN_RE.test(m[1])) return null;
200
- return m[1].split(".").join("/");
121
+ if (typeof profile.governable === "function") {
122
+ const v = profile.governable(root);
123
+ if (!v || v.ok !== true) return { ok: false, reason: (v && v.reason) || `profile "${profile.id}" refuses to record approvals in this tree` };
124
+ }
125
+ return { ok: true };
201
126
  }
202
127
 
203
- function kotlinFile(root, sourceSet, rel) {
204
- const packageDir = resolvePackageDir(root);
205
- if (!packageDir) return null;
206
- return path.posix.join(KOTLIN_SOURCE_SETS[sourceSet], packageDir, rel);
207
- }
128
+ // ── The neutral entries — helpers a profile's artifacts(root) composes ──────
208
129
 
209
130
  /**
210
- * Is the project's package resolvable at all? False in the raw template (the
211
- * namespace is still a placeholder token) and in any pre-stamp tree the tell
212
- * that this is not a generated project. The approve CLI refuses to WRITE
213
- * approvals in such a tree (recording decisions against a template pollutes
214
- * the template itself); read-only status remains available.
131
+ * One `feature-brief:<name>` per doc under docs/features/ LOCATION is the
132
+ * governance opt-in (CHANGE-FLOW-DESIGN.md §2); docs/proposals/ stays
133
+ * ungoverned harness-standards prose. They sit DIRECTLY after intent: the
134
+ * decide layer. At genesis the first feature's brief is drafted from the
135
+ * intent interview before architecture is even walked; post-genesis every
136
+ * decision-carrying change enters here. Approving one hashes the doc's bytes
137
+ * (with the declaration block stripped — hashFeatureBriefArtifact), so a
138
+ * signed brief cannot be quietly rewritten; acceptance is a LEDGER field on
139
+ * the same row (acceptFeature below), so the human's bookend never touches
140
+ * the signed bytes.
215
141
  * @param {string} root
216
- * @returns {boolean}
142
+ * @returns {Array<{id: string, label: string, files: string[], complete: boolean, hash: (root: string) => {hash: string, fileCount: number, missing: string[]}}>}
217
143
  */
218
- export function isPackageResolvable(root) {
219
- return resolvePackageDir(root) !== null;
144
+ export function featureBriefArtifacts(root) {
145
+ return listFeatureBriefs(root).map((brief) => ({
146
+ id: `${FEATURE_BRIEF_PREFIX}${brief.name}`,
147
+ label: `Feature brief (${brief.rel})`,
148
+ files: [brief.rel],
149
+ complete: true,
150
+ hash: (r) => hashFeatureBriefArtifact(r, { id: `${FEATURE_BRIEF_PREFIX}${brief.name}`, files: [brief.rel] }),
151
+ }));
220
152
  }
221
153
 
222
- // ── Components glob ─────────────────────────────────────────────────────────
223
-
224
154
  /**
225
- * Sorted list of `presentation/components/*.kt` files under the resolved
226
- * package, non-recursive (GENESIS-FLOW-DESIGN.md §1's `components` artifact — the
227
- * component vocabulary conversation 3 approves). Package-unresolvable or a
228
- * missing/empty directory both yield `[]` resolveArtifactStatus/approveArtifact
229
- * already treat a 0-file artifact as unresolvable ("a components glob matching
230
- * zero files is unresolvable, not approvable-empty" §1), so no special-casing
231
- * is needed here beyond returning the honest (possibly empty) list.
155
+ * Feature designs (brief design spec → build, decided 2026-07-25): one
156
+ * `feature-design:<name>` per BRIEF with a surface declared in the brief's
157
+ * cmp:feature block (`declares(block)`, so the gate exists before any file
158
+ * does) or evident on disk (`surfaceFiles(root, name)` non-empty). Signed on
159
+ * RENDERED output, never descriptions, BEFORE the behavior contract pins the
160
+ * form down. Briefs only, deliberately: legacy features never sprout
161
+ * retro-governance. With no surface files yet, `complete: false`
162
+ * approveArtifact refuses, exactly right: you cannot sign a design that has
163
+ * nothing rendered. What a surface IS (screens, an API contract diff) is the
164
+ * profile's answer.
232
165
  * @param {string} root
233
- * @returns {string[]} root-relative paths, sorted
166
+ * @param {{surfaceFiles: (root: string, name: string) => string[], declares: (block: object) => boolean, label?: (name: string) => string, complete?: (files: string[]) => boolean}} surface
167
+ * @returns {Array<{id: string, label: string, files: string[], complete: boolean}>}
234
168
  */
235
- function listComponentFiles(root) {
236
- const dirRel = kotlinFile(root, "commonMain", "presentation/components");
237
- if (!dirRel) return [];
238
- let entries;
239
- try {
240
- entries = fs.readdirSync(path.join(root, dirRel), { withFileTypes: true });
241
- } catch {
242
- return [];
169
+ export function featureDesignArtifacts(root, surface) {
170
+ const out = [];
171
+ for (const brief of listFeatureBriefs(root)) {
172
+ let declares = false;
173
+ try {
174
+ declares = Boolean(surface.declares(parseFeatureBlock(fs.readFileSync(path.join(root, brief.rel), "utf8"))));
175
+ } catch {
176
+ /* an unreadable brief declares nothing */
177
+ }
178
+ const files = surface.surfaceFiles(root, brief.name);
179
+ if (!declares && files.length === 0) continue;
180
+ const complete = surface.complete ? surface.complete(files) : files.length > 0;
181
+ const incompleteReason = complete ? undefined : surface.incompleteReason ? surface.incompleteReason(files) : undefined;
182
+ out.push({
183
+ id: `${FEATURE_DESIGN_PREFIX}${brief.name}`,
184
+ label: surface.label ? surface.label(brief.name) : `Feature design (${brief.name}, signed on rendered output)`,
185
+ files,
186
+ complete,
187
+ ...(incompleteReason ? { incompleteReason } : {}),
188
+ });
243
189
  }
244
- return entries
245
- .filter((e) => e.isFile() && e.name.endsWith(".kt"))
246
- .map((e) => path.posix.join(dirRel, e.name))
247
- .sort((a, b) => a.localeCompare(b));
190
+ return out;
248
191
  }
249
192
 
250
- // ── Feature screens glob ────────────────────────────────────────────────────
251
-
252
- /** The `feature-design:` id prefix one place, so the CLI/console/gate never drift on it. */
253
- export const FEATURE_DESIGN_PREFIX = "feature-design:";
193
+ /**
194
+ * One `feature-spec:<name>` per spec file present right now, minus the names
195
+ * the profile excludes (mobile: the base spec and the exemplar's own).
196
+ * @param {string} root
197
+ * @param {{specsDir?: string, exclude?: string[]}} [opts]
198
+ * @returns {Array<{id: string, label: string, files: string[], complete: boolean}>}
199
+ */
200
+ export function featureSpecArtifacts(root, { specsDir = "specs", exclude = [] } = {}) {
201
+ const dir = path.join(root, ...specsDir.split("/"));
202
+ if (!fs.existsSync(dir)) return [];
203
+ return fs
204
+ .readdirSync(dir)
205
+ .filter((f) => f.endsWith(".spec.md") && !exclude.includes(f))
206
+ .sort((a, b) => a.localeCompare(b))
207
+ .map((file) => ({
208
+ id: `feature-spec:${file.slice(0, -".spec.md".length)}`,
209
+ label: `Feature spec (${specsDir}/${file})`,
210
+ files: [`${specsDir}/${file}`],
211
+ complete: true,
212
+ }));
213
+ }
254
214
 
255
215
  /**
256
- * The screen files of one feature `presentation/<name>/**\/*Screen.kt`,
257
- * recursive, sorted. DELIBERATELY only `*Screen.kt`: the design signature
258
- * covers the FORM (what renders), so binding the whole presentation dir would
259
- * make every ViewModel edit during a legitimate build read as design drift.
216
+ * The `architecture` artifact: a structural spec plus the architecture doc
217
+ * with its generated sections stripped (hashArchitectureArtifact). Which
218
+ * files those are is the profile's; the hashing rule is the core's.
260
219
  * @param {string} root
261
- * @param {string} name the feature name (presentation/<name>/)
262
- * @returns {string[]} repo-relative posix paths
220
+ * @param {{specRel: string, docRel: string}} files
221
+ * @returns {{id: string, label: string, files: string[], complete: boolean, hash: (root: string) => {hash: string, fileCount: number, missing: string[]}}}
263
222
  */
264
- function listFeatureScreenFiles(root, name) {
265
- const dirRel = kotlinFile(root, "commonMain", `presentation/${name}`);
266
- if (!dirRel) return [];
267
- const out = [];
268
- const walk = (rel) => {
269
- let entries;
270
- try {
271
- entries = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
272
- } catch {
273
- return;
274
- }
275
- for (const e of entries) {
276
- const childRel = path.posix.join(rel, e.name);
277
- if (e.isDirectory()) walk(childRel);
278
- else if (e.isFile() && e.name.endsWith("Screen.kt")) out.push(childRel);
279
- }
223
+ export function architectureArtifact(root, { specRel, docRel }) {
224
+ return {
225
+ id: "architecture",
226
+ label: `Architecture + structure (${specRel} + ${docRel}, generated sections stripped)`,
227
+ files: [specRel, docRel],
228
+ complete: true,
229
+ hash: (r) => hashArchitectureArtifact(r, { specRel, docRel }),
280
230
  };
281
- walk(dirRel);
282
- return out.sort((a, b) => a.localeCompare(b));
283
231
  }
284
232
 
285
233
  // ── Registry ─────────────────────────────────────────────────────────────────
@@ -309,132 +257,21 @@ function listFeatureScreenFiles(root, name) {
309
257
  * @returns {Array<{id: string, label: string, files: string[], complete: boolean}>}
310
258
  */
311
259
  export function listGovernedArtifacts(root) {
312
- const artifacts = [];
313
- const packageResolved = resolvePackageDir(root) !== null;
314
-
315
- artifacts.push({
316
- id: "intent",
317
- label: `Intent brief (${INTENT_REL})`,
318
- files: [INTENT_REL],
319
- complete: true,
320
- });
321
-
322
- // Feature briefs (feature-brief.mjs): one `feature-brief:<name>` per doc
323
- // under docs/features/ — LOCATION is the governance opt-in (CHANGE-FLOW-
324
- // DESIGN.md §2); docs/proposals/ stays ungoverned harness-standards prose.
325
- // They sit DIRECTLY after intent: the decide layer. At genesis the first
326
- // feature's brief is drafted from the intent interview before architecture
327
- // is even walked; post-genesis every decision-carrying change enters here.
328
- // Approving one hashes the doc's bytes, so a signed brief cannot be quietly
329
- // rewritten; acceptance is a LEDGER field on the same row (acceptFeature
330
- // below), so the human's bookend never touches the signed bytes.
331
- for (const brief of listFeatureBriefs(root)) {
332
- artifacts.push({
333
- id: `feature-brief:${brief.name}`,
334
- label: `Feature brief (${brief.rel})`,
335
- files: [brief.rel],
336
- complete: true,
337
- });
338
- }
339
-
340
- artifacts.push({
341
- id: "architecture",
342
- label: `Architecture + structure (${ARCHITECTURE_SPEC_REL} + ${ARCH_DOC_REL_PATH}, generated sections stripped)`,
343
- // Hashed via hashArchitectureArtifact (spec bytes + stripped-doc content),
344
- // NOT the generic hashArtifactFiles — this list is still the artifact's
345
- // expected-files surface (missing-file refusal messages, "what governs
346
- // this" bookkeeping), just not what gets hashed raw. See computeArtifactHash.
347
- files: [ARCHITECTURE_SPEC_REL, ARCH_DOC_REL_PATH],
348
- complete: true,
349
- });
350
-
351
- const { f: exemplarF, F: exemplarF_Pascal, E: exemplarE } = resolveExemplarNames(root);
352
- const exemplarSpecRel = `specs/${exemplarF}.spec.md`;
353
- const exemplarKotlinFiles = exemplarKotlinFileSet(exemplarF_Pascal, exemplarF, exemplarE);
354
-
355
- // Spec-first: the exemplar's behavior clauses are confirmed BEFORE the slice
356
- // is built — the definition order is the discipline, not just a display order.
357
- artifacts.push({
358
- id: "exemplar-spec",
359
- label: `Exemplar spec (${exemplarSpecRel})`,
360
- files: [exemplarSpecRel],
361
- complete: true,
362
- });
363
-
364
- artifacts.push({
365
- id: "exemplar-feature",
366
- label: `Exemplar feature (${exemplarF} — the file set the stamper clones)`,
367
- files: [
368
- ...exemplarKotlinFiles.map((f) => kotlinFile(root, f.sourceSet, f.rel)).filter(Boolean),
369
- exemplarSpecRel,
370
- ],
371
- complete: packageResolved,
372
- });
373
-
374
- // UI-first: the design system LOCKS on the real exemplar (candidates render on
375
- // real screens, never stubs), and the component vocabulary is DISTILLED from
376
- // those screens — both follow the exemplar in the definition order.
377
- artifacts.push({
378
- id: "design-system",
379
- label: "Design system (presentation/theme/Theme.kt, Tokens.kt)",
380
- files: [
381
- kotlinFile(root, "commonMain", "presentation/theme/Theme.kt"),
382
- kotlinFile(root, "commonMain", "presentation/theme/Tokens.kt"),
383
- ].filter(Boolean),
384
- complete: packageResolved,
385
- });
386
-
387
- artifacts.push({
388
- id: "components",
389
- label: "Components (presentation/components/*.kt)",
390
- files: listComponentFiles(root),
391
- complete: packageResolved,
392
- });
393
-
394
- // Feature designs (brief → design → spec → build, decided 2026-07-25): one
395
- // `feature-design:<name>` per BRIEF with a UI surface — declared
396
- // (`"screens": true` in the cmp:feature block, so the gate exists before any
397
- // file does) or evident (presentation/<name>/*Screen.kt on disk). Signed on
398
- // RENDERED output, never descriptions, BEFORE the behavior contract pins the
399
- // form down. Briefs only, deliberately: legacy features (governed by
400
- // exemplar-feature or nothing) never sprout retro-governance. With no screen
401
- // files yet, `complete: false` — approveArtifact refuses, exactly right: you
402
- // cannot sign a design that has nothing rendered.
403
- for (const brief of listFeatureBriefs(root)) {
404
- let declaresScreens = false;
405
- try {
406
- declaresScreens = parseFeatureBlock(fs.readFileSync(path.join(root, brief.rel), "utf8")).screens;
407
- } catch {
408
- /* an unreadable brief declares nothing */
409
- }
410
- const screenFiles = listFeatureScreenFiles(root, brief.name);
411
- if (!declaresScreens && screenFiles.length === 0) continue;
412
- artifacts.push({
413
- id: `${FEATURE_DESIGN_PREFIX}${brief.name}`,
414
- label: `Feature design (${brief.name} — presentation/${brief.name}/*Screen.kt, signed on rendered output)`,
415
- files: screenFiles,
416
- complete: packageResolved && screenFiles.length > 0,
417
- });
260
+ const profile = profileFor(root);
261
+ if (typeof profile.artifacts !== "function") {
262
+ // A profile that declares no artifact model still has the Decide layer:
263
+ // briefs are the workflow loop's, not a stack's. Nothing else is assumed.
264
+ return featureBriefArtifacts(root);
418
265
  }
419
-
420
- const specsDir = path.join(root, "specs");
421
- if (fs.existsSync(specsDir)) {
422
- const featureSpecs = fs
423
- .readdirSync(specsDir)
424
- .filter((f) => f.endsWith(".spec.md") && f !== "app-base.spec.md" && f !== `${exemplarF}.spec.md`)
425
- .sort((a, b) => a.localeCompare(b));
426
- for (const file of featureSpecs) {
427
- const name = file.slice(0, -".spec.md".length);
428
- artifacts.push({
429
- id: `feature-spec:${name}`,
430
- label: `Feature spec (specs/${file})`,
431
- files: [`specs/${file}`],
432
- complete: true,
433
- });
434
- }
266
+ const list = profile.artifacts(root);
267
+ if (!Array.isArray(list)) throw new Error(`profile "${profile.id}" artifacts(root) must return an array`);
268
+ const seen = new Set();
269
+ for (const a of list) {
270
+ if (!a || typeof a.id !== "string" || !a.id || !Array.isArray(a.files)) throw new Error(`profile "${profile.id}" returned a malformed artifact: ${JSON.stringify(a)}`);
271
+ if (seen.has(a.id)) throw new Error(`profile "${profile.id}" lists artifact "${a.id}" twice`);
272
+ seen.add(a.id);
435
273
  }
436
-
437
- return artifacts;
274
+ return list;
438
275
  }
439
276
 
440
277
  // ── Hashing (mirrors qa/lib/inputs-hash.mjs's computeInputsHash style) ───────
@@ -477,7 +314,7 @@ export function hashArtifactFiles(root, relFiles) {
477
314
 
478
315
  /**
479
316
  * The `architecture` artifact's hash basis (docs/proposals/architecture-document-
480
- * standard.md §4.4): `${ARCHITECTURE_SPEC_REL}`'s raw bytes + `${ARCH_DOC_REL_PATH}`
317
+ * standard.md §4.4): the structural spec's raw bytes + the architecture doc
481
318
  * with every `cmp:generated` marker's BODY stripped — `arch-doc.mjs`'s
482
319
  * `stripGeneratedSections` is the ONE definition of "generated" for that doc,
483
320
  * reused here rather than forked, so a new/changed marker id is understood
@@ -501,21 +338,22 @@ export function hashArtifactFiles(root, relFiles) {
501
338
  * `cmp:generated` marker itself (structural, not generated content) — changes
502
339
  * it, same as editing the spec.
503
340
  * @param {string} root
341
+ * @param {{specRel: string, docRel: string}} files which spec and which doc — the profile's
504
342
  * @returns {{ hash: string, fileCount: number, missing: string[] }}
505
343
  */
506
- export function hashArchitectureArtifact(root) {
344
+ export function hashArchitectureArtifact(root, { specRel, docRel }) {
507
345
  const rows = [];
508
346
  const missing = [];
509
347
 
510
348
  try {
511
- const specBytes = fs.readFileSync(path.join(root, ARCHITECTURE_SPEC_REL));
512
- rows.push([ARCHITECTURE_SPEC_REL, createHash("sha256").update(specBytes).digest("hex")]);
349
+ const specBytes = fs.readFileSync(path.join(root, specRel));
350
+ rows.push([specRel, createHash("sha256").update(specBytes).digest("hex")]);
513
351
  } catch {
514
- missing.push(ARCHITECTURE_SPEC_REL);
352
+ missing.push(specRel);
515
353
  }
516
354
 
517
355
  try {
518
- const docRaw = fs.readFileSync(path.join(root, ARCH_DOC_REL_PATH), "utf8");
356
+ const docRaw = fs.readFileSync(path.join(root, docRel), "utf8");
519
357
  // Normalize line endings BEFORE stripping: the marker grammar
520
358
  // (`arch-doc.mjs`'s MARKER_BLOCK_RE) matches a literal `\n` right after
521
359
  // `-->`, so CRLF content would fail to match at all and nothing would be
@@ -523,9 +361,9 @@ export function hashArchitectureArtifact(root) {
523
361
  // hash itself.
524
362
  const docNormalized = docRaw.replace(/\r\n/g, "\n");
525
363
  const docStripped = stripGeneratedSections(docNormalized);
526
- rows.push([ARCH_DOC_REL_PATH, createHash("sha256").update(docStripped, "utf8").digest("hex")]);
364
+ rows.push([docRel, createHash("sha256").update(docStripped, "utf8").digest("hex")]);
527
365
  } catch {
528
- missing.push(ARCH_DOC_REL_PATH);
366
+ missing.push(docRel);
529
367
  }
530
368
 
531
369
  // Code-unit sort for the same reason as hashArtifactFiles: hash order must
@@ -539,9 +377,11 @@ export function hashArchitectureArtifact(root) {
539
377
  }
540
378
 
541
379
  /**
542
- * Recompute one artifact's hash — `hashArchitectureArtifact` for `architecture`
543
- * (spec + stripped doc, its own basis), `hashArtifactFiles(root, artifact.files)`
544
- * for every other artifact (raw file bytes). The ONE dispatch point
380
+ * Recompute one artifact's hash — the artifact's own `hash(root)` when the
381
+ * profile attached one (the architecture artifact: spec + stripped doc), the
382
+ * brief rule for `feature-brief:*` (declaration block stripped a core
383
+ * mechanic, so a bare {id, files} brief still hashes right), else
384
+ * `hashArtifactFiles(root, artifact.files)` (raw file bytes). The ONE dispatch point
545
385
  * `resolveArtifactStatus`/`approveArtifact` both call, so the two never
546
386
  * disagree about what "the architecture artifact's hash" means.
547
387
  * @param {string} root
@@ -549,7 +389,7 @@ export function hashArchitectureArtifact(root) {
549
389
  * @returns {{ hash: string, fileCount: number, missing: string[] }}
550
390
  */
551
391
  function computeArtifactHash(root, artifact) {
552
- if (artifact.id === "architecture") return hashArchitectureArtifact(root);
392
+ if (typeof artifact.hash === "function") return artifact.hash(root);
553
393
  if (artifact.id.startsWith(FEATURE_BRIEF_PREFIX)) return hashFeatureBriefArtifact(root, artifact);
554
394
  return hashArtifactFiles(root, artifact.files);
555
395
  }
@@ -602,9 +442,10 @@ function hashFeatureBriefArtifact(root, artifact) {
602
442
  * "unreviewed" and every exemplar lookup to the default (`home`). Never throws.
603
443
  *
604
444
  * `exemplarFeature` is `undefined` when the key is absent or not a non-empty
605
- * string — callers resolve the default (`getExemplarFeature`), never this
606
- * function directly, so every ledger written before this key existed keeps
607
- * meaning what it meant (GENESIS-FLOW-DESIGN.md §1).
445
+ * string — a profile that uses the key (mobile's configurable exemplar,
446
+ * profiles/cmp/artifacts.mjs getExemplarFeature) resolves its own default, never
447
+ * this function, so every ledger written before the key existed keeps meaning
448
+ * what it meant (GENESIS-FLOW-DESIGN.md §1).
608
449
  * @param {string} root
609
450
  * @returns {{ schema: string, artifacts: Array<{artifact: string, status: string, hash: (string|null), approvedAt: (string|null), mode?: string, reopenedAt?: string}>, exemplarFeature: (string|undefined) }}
610
451
  */
@@ -698,20 +539,6 @@ export function readJournal(root) {
698
539
  return events;
699
540
  }
700
541
 
701
- /**
702
- * The configured exemplar feature's lowercase name (the package-segment form,
703
- * e.g. `"home"`, `"favorites"`) — `qa/approvals.json`'s top-level
704
- * `exemplarFeature` key, defaulting to `"home"` when absent (GENESIS-FLOW-DESIGN.md
705
- * §1). This is the ONE function both `resolveExemplarNames` (registry) and
706
- * qa/scaffold-feature.mjs (clone-source resolution) call — never read the raw key
707
- * directly, so the default lives in exactly one place.
708
- * @param {string} root
709
- * @returns {string}
710
- */
711
- export function getExemplarFeature(root) {
712
- return loadApprovals(root).exemplarFeature ?? "home";
713
- }
714
-
715
542
  /**
716
543
  * Seed one artifact as unreviewed if it isn't already recorded. Idempotent —
717
544
  * a second call for the same id is a no-op. Used by qa/scaffold-feature.mjs to
@@ -909,9 +736,9 @@ export function approveArtifact(root, artifactId, options = {}) {
909
736
  return {
910
737
  ok: false,
911
738
  reason:
912
- `cannot approve "${artifactId}" — its file set cannot be fully resolved: the kotlin-rooted files are unresolvable because ` +
913
- "the project package is not resolvable from composeApp/build.gradle.kts (likely the raw template or a pre-stamp tree " +
914
- `run this in a generated project); only ${resolved.fileCount} file(s) resolved. ` +
739
+ `cannot approve "${artifactId}" — its file set cannot be fully resolved: ` +
740
+ `${artifact.incompleteReason || "the profile marked it incomplete (typically the project is not yet stamped, so source-rooted files cannot be located)"}; ` +
741
+ `only ${resolved.fileCount} file(s) resolved. ` +
915
742
  "A partial or empty approval is vacuous (it attests nothing for the unresolved files) and is refused.",
916
743
  };
917
744
  }