@voxgig/sdkgen 3.4.7 → 3.5.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/bin/voxgig-sdkgen +78 -10
- package/dist/action/action.d.ts +5 -2
- package/dist/action/action.js +113 -6
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.d.ts +15 -0
- package/dist/action/check.js +347 -0
- package/dist/action/check.js.map +1 -0
- package/dist/action/dispatch.d.ts +8 -0
- package/dist/action/dispatch.js +74 -0
- package/dist/action/dispatch.js.map +1 -0
- package/dist/action/docs.d.ts +4 -0
- package/dist/action/docs.js +236 -0
- package/dist/action/docs.js.map +1 -0
- package/dist/action/doctor.d.ts +5 -2
- package/dist/action/doctor.js +526 -74
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/feature.js +96 -17
- package/dist/action/feature.js.map +1 -1
- package/dist/action/kind.d.ts +30 -0
- package/dist/action/kind.js +258 -0
- package/dist/action/kind.js.map +1 -0
- package/dist/action/package.d.ts +25 -0
- package/dist/action/package.js +852 -0
- package/dist/action/package.js.map +1 -0
- package/dist/action/resolve.d.ts +18 -0
- package/dist/action/resolve.js +280 -0
- package/dist/action/resolve.js.map +1 -0
- package/dist/action/target.d.ts +7 -1
- package/dist/action/target.js +240 -97
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Docs.d.ts +4 -0
- package/dist/cmp/Docs.js +98 -0
- package/dist/cmp/Docs.js.map +1 -0
- package/dist/cmp/ExternalDocs.d.ts +2 -0
- package/dist/cmp/ExternalDocs.js +43 -0
- package/dist/cmp/ExternalDocs.js.map +1 -0
- package/dist/helpers/definition.d.ts +5 -0
- package/dist/helpers/definition.js +72 -0
- package/dist/helpers/definition.js.map +1 -0
- package/dist/helpers/featureSource.d.ts +8 -2
- package/dist/helpers/featureSource.js +61 -12
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/junk.d.ts +11 -0
- package/dist/helpers/junk.js +149 -0
- package/dist/helpers/junk.js.map +1 -0
- package/dist/helpers/manifest.d.ts +50 -0
- package/dist/helpers/manifest.js +328 -0
- package/dist/helpers/manifest.js.map +1 -0
- package/dist/helpers/modelcheck.d.ts +22 -0
- package/dist/helpers/modelcheck.js +203 -0
- package/dist/helpers/modelcheck.js.map +1 -0
- package/dist/helpers/semver.d.ts +4 -0
- package/dist/helpers/semver.js +174 -0
- package/dist/helpers/semver.js.map +1 -0
- package/dist/helpers/shipped.d.ts +4 -0
- package/dist/helpers/shipped.js +36 -0
- package/dist/helpers/shipped.js.map +1 -0
- package/dist/helpers/stdrep.d.ts +9 -1
- package/dist/helpers/stdrep.js +22 -0
- package/dist/helpers/stdrep.js.map +1 -1
- package/dist/sdkgen.d.ts +15 -3
- package/dist/sdkgen.js +233 -67
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/utility.js +17 -0
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aontu +69 -0
- package/package.json +3 -2
- package/project/.sdk/model/feature/audit.aontu +1 -0
- package/project/.sdk/model/feature/cache.aontu +1 -0
- package/project/.sdk/model/feature/clienttrack.aontu +1 -0
- package/project/.sdk/model/feature/debug.aontu +1 -0
- package/project/.sdk/model/feature/idempotency.aontu +1 -0
- package/project/.sdk/model/feature/log.aontu +1 -0
- package/project/.sdk/model/feature/metrics.aontu +1 -0
- package/project/.sdk/model/feature/netsim.aontu +1 -0
- package/project/.sdk/model/feature/paging.aontu +1 -0
- package/project/.sdk/model/feature/proxy.aontu +1 -0
- package/project/.sdk/model/feature/ratelimit.aontu +1 -0
- package/project/.sdk/model/feature/rbac.aontu +1 -0
- package/project/.sdk/model/feature/retry.aontu +1 -0
- package/project/.sdk/model/feature/streaming.aontu +1 -0
- package/project/.sdk/model/feature/telemetry.aontu +1 -0
- package/project/.sdk/model/feature/test.aontu +1 -0
- package/project/.sdk/model/feature/timeout.aontu +1 -0
- package/project/.sdk/model/target/c.aontu +1 -0
- package/project/.sdk/model/target/clojure.aontu +1 -0
- package/project/.sdk/model/target/cpp.aontu +1 -0
- package/project/.sdk/model/target/dart.aontu +1 -0
- package/project/.sdk/model/target/elixir.aontu +1 -0
- package/project/.sdk/model/target/go-cli.aontu +1 -0
- package/project/.sdk/model/target/go-mcp.aontu +1 -0
- package/project/.sdk/model/target/go.aontu +1 -0
- package/project/.sdk/model/target/haskell.aontu +1 -0
- package/project/.sdk/model/target/java.aontu +1 -0
- package/project/.sdk/model/target/js.aontu +1 -0
- package/project/.sdk/model/target/kotlin.aontu +1 -0
- package/project/.sdk/model/target/lean.aontu +1 -0
- package/project/.sdk/model/target/lua.aontu +1 -0
- package/project/.sdk/model/target/ocaml.aontu +1 -0
- package/project/.sdk/model/target/perl.aontu +1 -0
- package/project/.sdk/model/target/php.aontu +1 -0
- package/project/.sdk/model/target/py-data.aontu +1 -0
- package/project/.sdk/model/target/py.aontu +1 -0
- package/project/.sdk/model/target/rb.aontu +1 -0
- package/project/.sdk/model/target/rust.aontu +1 -0
- package/project/.sdk/model/target/scala.aontu +1 -0
- package/project/.sdk/model/target/seneca-provider.aontu +1 -0
- package/project/.sdk/model/target/zig.aontu +1 -0
- package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
- package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
- package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
- package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
- package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
- package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
- package/project/.sdk/src/cmp/dart/Config_dart.ts +43 -5
- package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
- package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
- package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
- package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
- package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
- package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
- package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
- package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
- package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
- package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
- package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
- package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
- package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
- package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
- package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
- package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
- package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
- package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
- package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
- package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -1
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/utility/make_options.c +5 -1
- package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
- package/project/.sdk/tm/cpp/core/types.hpp +4 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
- package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
- package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
- package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
- package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
- package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
- package/project/.sdk/tm/java/core/SdkClient.java +3 -1
- package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
- package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
- package/project/.sdk/tm/ocaml/Makefile +3 -2
- package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
- package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
- package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
- package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
- package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
- package/project/.sdk/tm/scala/utility/Make.scala +5 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
- package/project/.sdk/tm/zig/core/utility.zig +85 -1
- package/project/sdkgen-package.json +57 -0
- package/src/action/action.ts +136 -6
- package/src/action/check.ts +495 -0
- package/src/action/dispatch.ts +105 -0
- package/src/action/docs.ts +310 -0
- package/src/action/doctor.ts +692 -81
- package/src/action/feature.ts +109 -17
- package/src/action/kind.ts +375 -0
- package/src/action/package.ts +1173 -0
- package/src/action/resolve.ts +362 -0
- package/src/action/target.ts +290 -108
- package/src/cmp/Docs.ts +125 -0
- package/src/cmp/ExternalDocs.ts +54 -0
- package/src/helpers/definition.ts +82 -0
- package/src/helpers/featureSource.ts +83 -13
- package/src/helpers/junk.ts +168 -0
- package/src/helpers/manifest.ts +474 -0
- package/src/helpers/modelcheck.ts +262 -0
- package/src/helpers/semver.ts +211 -0
- package/src/helpers/shipped.ts +41 -0
- package/src/helpers/stdrep.ts +70 -0
- package/src/sdkgen.ts +294 -66
- package/src/types.ts +33 -3
- package/src/utility.ts +17 -0
package/dist/action/doctor.js
CHANGED
|
@@ -41,9 +41,25 @@ const types_1 = require("../types");
|
|
|
41
41
|
const utility_1 = require("../utility");
|
|
42
42
|
const stdrep_1 = require("../helpers/stdrep");
|
|
43
43
|
const target_1 = require("./target");
|
|
44
|
+
// `aliasModelText` is no longer named here: the model-file comparison asks
|
|
45
|
+
// the registry for the kind's own `rename`, so a kind that renames
|
|
46
|
+
// differently is handled without this file knowing how.
|
|
47
|
+
const kind_1 = require("./kind");
|
|
48
|
+
const resolve_1 = require("./resolve");
|
|
49
|
+
const definition_1 = require("../helpers/definition");
|
|
50
|
+
const featureSource_1 = require("../helpers/featureSource");
|
|
51
|
+
const junk_1 = require("../helpers/junk");
|
|
44
52
|
// jostraca's Copy walk skips these (IGNORED_RE in CopyOp) — editor backups and
|
|
45
53
|
// deliberately-disabled templates never reach a project, so they are not drift.
|
|
54
|
+
//
|
|
55
|
+
// `isJunk` is the other half of the same statement: what the WRITER refuses to
|
|
56
|
+
// copy, this reader must refuse to miss. A `__pycache__` in the scaffold would
|
|
57
|
+
// otherwise be reported as missing from every project on earth, and one in a
|
|
58
|
+
// project as an unexplained extra file. See helpers/junk.
|
|
46
59
|
const IGNORED_RE = /(~|-jostraca-off)$/;
|
|
60
|
+
function ignoredEntry(name) {
|
|
61
|
+
return IGNORED_RE.test(name) || (0, junk_1.isJunk)(name);
|
|
62
|
+
}
|
|
47
63
|
// Extensions jostraca copies byte-for-byte. Comparing them as text would
|
|
48
64
|
// report spurious differences, so they are compared by raw bytes.
|
|
49
65
|
const BINARY_RE = /\.(png|jpg|jpeg|gif|ico|pdf|zip|gz|woff2?|ttf|eot|wasm)$/i;
|
|
@@ -83,39 +99,85 @@ async function cmd_doctor_check(_args, actx) {
|
|
|
83
99
|
}
|
|
84
100
|
// Code API. Returns the report; the CLI turns a non-ok report into a
|
|
85
101
|
// non-zero exit so this can gate CI.
|
|
86
|
-
async function doctor(actx) {
|
|
102
|
+
async function doctor(actx, scope) {
|
|
87
103
|
const log = actx.log;
|
|
88
104
|
const fs = actx.fs();
|
|
89
105
|
const model = actx.model;
|
|
90
106
|
const root = actx.folder;
|
|
91
107
|
const report = {
|
|
92
108
|
forked: [], edited: [], stale: [], missing: [], additive: [],
|
|
93
|
-
unwired: [], ok: true,
|
|
109
|
+
unwired: [], resyncPending: [], aliasedDiff: [], ok: true,
|
|
94
110
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// EVERY KIND, not just targets.
|
|
112
|
+
//
|
|
113
|
+
// `add` writes a copied model file for each kind — `model/target/<t>.aontu`
|
|
114
|
+
// and `model/feature/<f>.aontu` alike — and overwrites it on every resync.
|
|
115
|
+
// Only the target one was ever compared, so a hand-edit to an installed
|
|
116
|
+
// FEATURE definition read as perfectly in sync and was silently reverted by
|
|
117
|
+
// the next `target add` (which re-runs `feature add` for every active
|
|
118
|
+
// feature). That is exactly the failure `checkTargetModel` was added for,
|
|
119
|
+
// left open for the other half of what add owns.
|
|
120
|
+
//
|
|
121
|
+
// Driven off the registry, so a kind added later is checked without
|
|
122
|
+
// anything here changing. The per-kind EXTRAS — a target's `src/cmp` and
|
|
123
|
+
// `tm` trees — stay in that kind's own check, because they are genuinely
|
|
124
|
+
// different work rather than the same work with different strings.
|
|
125
|
+
const kinds = Object.keys(kind_1.KINDS).sort();
|
|
126
|
+
const counts = {};
|
|
127
|
+
for (const kind of kinds) {
|
|
128
|
+
counts[kind] = Object.keys(model?.main?.[types_1.KIT]?.[kind] ?? {}).length;
|
|
129
|
+
}
|
|
130
|
+
log.info({ point: 'doctor-start', targets: counts.target ?? 0, ...counts });
|
|
131
|
+
// Targets resolved ONCE, before the kind loop, because the feature checks
|
|
132
|
+
// need them too: a feature's per-target source lives under the name the
|
|
133
|
+
// target has in its own source, so finding it means knowing where each
|
|
134
|
+
// target came from. Resolving per (feature, target) pair would re-read
|
|
135
|
+
// every package manifest dozens of times and warn repeatedly about the
|
|
136
|
+
// same unresolvable target.
|
|
137
|
+
//
|
|
138
|
+
// Resolved even when the run is SCOPED past them — a scoped check of one
|
|
139
|
+
// feature still has to look in every target's tree for that feature's
|
|
140
|
+
// files.
|
|
141
|
+
const targets = new Map();
|
|
142
|
+
for (const tname of Object.keys(model?.main?.[types_1.KIT]?.target ?? {}).sort()) {
|
|
143
|
+
const source = resolveDeclared('target', tname, actx);
|
|
144
|
+
if (null != source) {
|
|
145
|
+
targets.set(tname, source);
|
|
113
146
|
}
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
}
|
|
148
|
+
for (const kind of kinds) {
|
|
149
|
+
const items = Object.keys(model?.main?.[types_1.KIT]?.[kind] ?? {}).sort();
|
|
150
|
+
for (const name of items) {
|
|
151
|
+
if (null != scope && !scope(kind, name)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const source = 'target' === kind ?
|
|
155
|
+
targets.get(name) : resolveDeclared(kind, name, actx);
|
|
156
|
+
if (null == source) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if ('target' === kind) {
|
|
160
|
+
checkTarget(actx, source, report);
|
|
161
|
+
}
|
|
162
|
+
if ('docs' === kind) {
|
|
163
|
+
checkDocs(actx, source, report);
|
|
164
|
+
}
|
|
165
|
+
// Only an ACTIVE feature has source copied out; what an inactive one
|
|
166
|
+
// left behind is stale, and the target walk reports it as such.
|
|
167
|
+
if ('feature' === kind &&
|
|
168
|
+
false !== model?.main?.[types_1.KIT]?.feature?.[name]?.active) {
|
|
169
|
+
checkFeatureSource(actx, source, targets, report);
|
|
170
|
+
}
|
|
171
|
+
checkItemModel(actx, kind, source, report);
|
|
116
172
|
}
|
|
117
173
|
}
|
|
118
|
-
|
|
174
|
+
// Root-component wiring is a property of the PROJECT, not of any item, so a
|
|
175
|
+
// scoped run has no business reporting on it — `package update` asking "is
|
|
176
|
+
// this package's copy clean" should not also say the project never wired in
|
|
177
|
+
// `ReadmeTop`.
|
|
178
|
+
if (null == scope) {
|
|
179
|
+
checkWiring(actx, report);
|
|
180
|
+
}
|
|
119
181
|
report.ok = 0 === report.forked.length + report.edited.length +
|
|
120
182
|
report.stale.length + report.missing.length;
|
|
121
183
|
for (const [kind, note] of [
|
|
@@ -125,6 +187,8 @@ async function doctor(actx) {
|
|
|
125
187
|
['missing', 'MISSING (would be written by `target add`)'],
|
|
126
188
|
['additive', 'additive (project-owned, not drift)'],
|
|
127
189
|
['unwired', 'NOT WIRED IN (root capability this project is missing)'],
|
|
190
|
+
['resyncPending', 'RESYNC PENDING (predates provenance; `target add` updates it)'],
|
|
191
|
+
['aliasedDiff', 'aliased model differs from its origin (project-owned, not drift)'],
|
|
128
192
|
]) {
|
|
129
193
|
for (const file of report[kind]) {
|
|
130
194
|
log.info({ point: 'doctor-finding', kind, file, note: note + ': ' + file });
|
|
@@ -139,6 +203,8 @@ async function doctor(actx) {
|
|
|
139
203
|
missing: report.missing.length,
|
|
140
204
|
additive: report.additive.length,
|
|
141
205
|
unwired: report.unwired.length,
|
|
206
|
+
resyncPending: report.resyncPending.length,
|
|
207
|
+
aliasedDiff: report.aliasedDiff.length,
|
|
142
208
|
note: report.ok ?
|
|
143
209
|
('.sdk matches the scaffold (' + report.additive.length + ' additive)') :
|
|
144
210
|
('.sdk has drifted: ' + report.forked.length + ' forked, ' +
|
|
@@ -172,8 +238,45 @@ function checkWiring(actx, report) {
|
|
|
172
238
|
}
|
|
173
239
|
}
|
|
174
240
|
}
|
|
241
|
+
// Where one declared item came from, or undefined with the reason said out
|
|
242
|
+
// loud.
|
|
243
|
+
//
|
|
244
|
+
// The model records `base` and `origname` (written by add), and `origname` is
|
|
245
|
+
// what makes an ALIAS checkable: the ref used to be rebuilt as
|
|
246
|
+
// `<base>/../<name>`, which names the INSTALLED item — so `target add ts~ts2`
|
|
247
|
+
// sent doctor looking for a `ts2` scaffold that does not exist, both trees
|
|
248
|
+
// walked empty, and every component read as `additive` while every template
|
|
249
|
+
// read as `stale` (a FAILING category). Real edits were undetectable and
|
|
250
|
+
// doctor went red on noise.
|
|
251
|
+
//
|
|
252
|
+
// A bare name falls back to the bundled scaffold, for a copy predating
|
|
253
|
+
// provenance.
|
|
254
|
+
function resolveDeclared(kind, name, actx) {
|
|
255
|
+
const declared = actx.model?.main?.[types_1.KIT]?.[kind]?.[name];
|
|
256
|
+
const ref = (0, kind_1.recordedRef)(declared, name) || name;
|
|
257
|
+
try {
|
|
258
|
+
// WITH THE LOG. Resolution reads the source's package manifest, and an
|
|
259
|
+
// unusable one is reported only through `ctx$.log`. Without it doctor
|
|
260
|
+
// silently lost the package name — and then reported every item from that
|
|
261
|
+
// package as FORKED, because the expected text no longer carries the
|
|
262
|
+
// `package:` line the copy has, with nothing anywhere saying why. That is
|
|
263
|
+
// accurate (the next add really would strip the line) but unactionable,
|
|
264
|
+
// which is the same defect as being wrong.
|
|
265
|
+
return (0, resolve_1.resolveSource)(ref, kind, { folder: actx.folder, fs: actx.fs, log: actx.log });
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
actx.log.warn({
|
|
269
|
+
point: 'doctor-source-unresolved', kind, [kind]: name, err: err.message,
|
|
270
|
+
note: name + ': cannot find its ' + kind + ' source (' +
|
|
271
|
+
err.message + ')'
|
|
272
|
+
});
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
175
276
|
function checkTarget(actx, resolved, report) {
|
|
176
|
-
const
|
|
277
|
+
const tname = resolved.name;
|
|
278
|
+
const tfolder = resolved.folder;
|
|
279
|
+
const torigname = resolved.origname;
|
|
177
280
|
const fs = actx.fs();
|
|
178
281
|
const model = actx.model;
|
|
179
282
|
const root = actx.folder;
|
|
@@ -182,12 +285,26 @@ function checkTarget(actx, resolved, report) {
|
|
|
182
285
|
//
|
|
183
286
|
// src/cmp — copied verbatim, so a byte compare is the truth.
|
|
184
287
|
// tm — copied through jostraca's template(), with ProjectName.
|
|
288
|
+
// An ALIASED install renames every `<Cmp>_<origname>` component to
|
|
289
|
+
// `<Cmp>_<alias>` and rewrites the origin name inside the file (sibling
|
|
290
|
+
// imports, the __dirname-relative fragment path), because components are
|
|
291
|
+
// dispatched by the convention `cmp/<t>/Main_<t>`. doctor has to apply the
|
|
292
|
+
// same two transforms or it expects the ORIGIN names in the alias's folder
|
|
293
|
+
// and reports the entire tree as missing — 25 files for `go~go2`, none of
|
|
294
|
+
// them a real finding.
|
|
295
|
+
const aliased = tname !== torigname;
|
|
296
|
+
const renameCmp = aliased ?
|
|
297
|
+
(rel) => (0, target_1.aliasCmpName)(rel, torigname, tname) : undefined;
|
|
298
|
+
const rewriteCmp = aliased ?
|
|
299
|
+
(src) => (0, target_1.aliasCmpText)(src, torigname, tname) : undefined;
|
|
185
300
|
const trees = [
|
|
186
301
|
{
|
|
187
302
|
project: node_path_1.default.join(root, 'src', 'cmp', tname),
|
|
188
303
|
scaffold: node_path_1.default.join(tfolder, 'src', 'cmp', torigname),
|
|
189
304
|
replace: {},
|
|
190
305
|
kind: 'forked',
|
|
306
|
+
rename: renameCmp,
|
|
307
|
+
rewrite: rewriteCmp,
|
|
191
308
|
},
|
|
192
309
|
{
|
|
193
310
|
project: node_path_1.default.join(root, 'tm', tname),
|
|
@@ -205,14 +322,71 @@ function checkTarget(actx, resolved, report) {
|
|
|
205
322
|
'test',
|
|
206
323
|
...Object.keys(featuremodel).filter((n) => false !== featuremodel[n]?.active),
|
|
207
324
|
]));
|
|
208
|
-
|
|
325
|
+
// `folder` and `model` matter: the trim catalogue is resolved consumer-side
|
|
326
|
+
// (see featureCatalogue), so a doctor that withheld them would compute a
|
|
327
|
+
// different trim from the one `target add` applied and report correctly
|
|
328
|
+
// trimmed files as missing.
|
|
329
|
+
const excludes = (0, target_1.trimFeatures)({ log: quietLog(actx.log), fs: () => fs, folder: root, model }, tfolder, torigname, tname, features);
|
|
330
|
+
compareTrees(actx, report, trees, {
|
|
331
|
+
excludes,
|
|
332
|
+
// Only a TARGET has foreign feature source landing in its tree.
|
|
333
|
+
foreign: (kind) => 'edited' === kind ?
|
|
334
|
+
foreignFeatureSource(actx, resolved) : [],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// COMPARE A SET OF TREES against the sources they were copied from.
|
|
338
|
+
//
|
|
339
|
+
// Extracted from `checkTarget` unchanged, because the docs kind needs exactly
|
|
340
|
+
// this and a second copy of it is the defect this workstream keeps producing.
|
|
341
|
+
// What differs per kind stays in the caller: which trees, what the copy
|
|
342
|
+
// substituted, whether a trim excludes part of the source, and whether
|
|
343
|
+
// anything foreign is expected to land in the tree.
|
|
344
|
+
//
|
|
345
|
+
// This is the mechanism behind the rule in CLAUDE.md — anything an add
|
|
346
|
+
// writes, doctor must compare, or the next add silently reverts a project's
|
|
347
|
+
// edit. A kind that declares trees and is not walked here would break it.
|
|
348
|
+
function compareTrees(actx, report, trees, opts) {
|
|
349
|
+
const fs = actx.fs();
|
|
350
|
+
const model = actx.model;
|
|
351
|
+
const root = actx.folder;
|
|
352
|
+
const excludes = opts?.excludes ?? [];
|
|
209
353
|
for (const tree of trees) {
|
|
210
354
|
// Findings are reported at project-relative paths, the way a maintainer
|
|
211
355
|
// would type them.
|
|
212
356
|
const label = node_path_1.default.relative(root, tree.project).split(node_path_1.default.sep).join('/') + '/';
|
|
213
|
-
const
|
|
357
|
+
const scaffoldFiles = 'edited' === tree.kind ?
|
|
214
358
|
walk(fs, tree.scaffold).filter((rel) => !excluded(rel, excludes)) :
|
|
215
359
|
walk(fs, tree.scaffold);
|
|
360
|
+
// Where each file the project should have COMES FROM: the name it lands
|
|
361
|
+
// under, mapped to the absolute path of the file it was copied from.
|
|
362
|
+
//
|
|
363
|
+
// An absolute path rather than a tree-relative one, because not every
|
|
364
|
+
// expected file comes from THIS tree — see the foreign-feature union
|
|
365
|
+
// below.
|
|
366
|
+
const landed = new Map(scaffoldFiles.map((rel) => [
|
|
367
|
+
null == tree.rename ? rel : tree.rename(rel),
|
|
368
|
+
node_path_1.default.join(tree.scaffold, rel),
|
|
369
|
+
]));
|
|
370
|
+
// FOREIGN FEATURE SOURCE.
|
|
371
|
+
//
|
|
372
|
+
// A feature supplied by a different package than the target ships its
|
|
373
|
+
// per-target source in ITS OWN `tm/<target>/` overlay, and `feature add`
|
|
374
|
+
// copies that into the project. Compared against the target's scaffold
|
|
375
|
+
// alone, those files are present in the project and absent upstream — so
|
|
376
|
+
// every one of them was reported STALE, a FAILING category. Any project
|
|
377
|
+
// using an external feature had a red `doctor` and nothing wrong with it.
|
|
378
|
+
//
|
|
379
|
+
// They are not stale, they are expected — just expected from somewhere
|
|
380
|
+
// else. Adding them here rather than suppressing them keeps them
|
|
381
|
+
// CHECKED: a hand-edit to a foreign feature's source is still reported,
|
|
382
|
+
// which is what `package update`'s gate needs in order to cover
|
|
383
|
+
// everything the re-add writes.
|
|
384
|
+
const foreign = new Set();
|
|
385
|
+
for (const [rel, from] of (opts?.foreign?.(tree.kind) ?? [])) {
|
|
386
|
+
landed.set(rel, from);
|
|
387
|
+
foreign.add(rel);
|
|
388
|
+
}
|
|
389
|
+
const expected = Array.from(landed.keys()).sort();
|
|
216
390
|
const actual = walk(fs, tree.project);
|
|
217
391
|
const expectedSet = new Set(expected);
|
|
218
392
|
const actualSet = new Set(actual);
|
|
@@ -221,7 +395,16 @@ function checkTarget(actx, resolved, report) {
|
|
|
221
395
|
report.missing.push(label + rel);
|
|
222
396
|
continue;
|
|
223
397
|
}
|
|
224
|
-
|
|
398
|
+
// A foreign feature's file is EXPECTED here (so it is not stale) but
|
|
399
|
+
// compared by `checkFeatureSource`, from the feature's side. Comparing
|
|
400
|
+
// it here too would report it twice on a full run — and, worse, would
|
|
401
|
+
// leave it uncompared on a run scoped to the feature alone, which is
|
|
402
|
+
// exactly when `feature add` is about to rewrite it.
|
|
403
|
+
if (foreign.has(rel)) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const from = landed.get(rel);
|
|
407
|
+
if (differs(fs, from, node_path_1.default.join(tree.project, rel), model, tree.replace, undefined, tree.rewrite)) {
|
|
225
408
|
report[tree.kind].push(label + rel);
|
|
226
409
|
}
|
|
227
410
|
}
|
|
@@ -232,7 +415,8 @@ function checkTarget(actx, resolved, report) {
|
|
|
232
415
|
// A component the scaffold has NEVER shipped is the project's own —
|
|
233
416
|
// the supported way to add a per-target component. Anything else under
|
|
234
417
|
// a tree `target add` owns is stale output.
|
|
235
|
-
const known = fs.existsSync(node_path_1.default.join(tree.scaffold, rel))
|
|
418
|
+
const known = fs.existsSync(node_path_1.default.join(tree.scaffold, rel)) ||
|
|
419
|
+
landed.has(rel);
|
|
236
420
|
if ('forked' === tree.kind && !known) {
|
|
237
421
|
report.additive.push(label + rel);
|
|
238
422
|
}
|
|
@@ -241,52 +425,299 @@ function checkTarget(actx, resolved, report) {
|
|
|
241
425
|
}
|
|
242
426
|
}
|
|
243
427
|
}
|
|
244
|
-
checkTargetModel(actx, resolved, report);
|
|
245
428
|
}
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
429
|
+
// A DOCS item's trees.
|
|
430
|
+
//
|
|
431
|
+
// Simpler than a target's in every way that matters here: no trim (a docs
|
|
432
|
+
// item has no per-feature source to leave out), and nothing foreign lands in
|
|
433
|
+
// them (a feature package's overlay targets a TARGET's tree). What is the
|
|
434
|
+
// same is the alias handling — docs components are dispatched by the same
|
|
435
|
+
// `Main_<n>` convention — and the tree paths, which come from the registry
|
|
436
|
+
// rather than being spelled a second time.
|
|
249
437
|
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
// as it overwrites src/cmp and tm. So a maintainer who fixes a dep version
|
|
254
|
-
// there loses the fix on the next resync with nothing said (the SDK regresses
|
|
255
|
-
// with nobody touching it, which is the failure the "a project decision
|
|
256
|
-
// belongs in the MODEL" rule exists to prevent), and a project whose copy
|
|
257
|
-
// predates a scaffold change carries the old declaration forever while doctor
|
|
258
|
-
// reported the target in sync. Both are the drift this command exists to
|
|
259
|
-
// name; the file was simply in neither tree it walked.
|
|
260
|
-
function checkTargetModel(actx, resolved, report) {
|
|
261
|
-
const { tname, tfolder, torigname, base } = resolved;
|
|
438
|
+
// The optional template tree is skipped when the SOURCE does not ship one:
|
|
439
|
+
// `docs add` did not copy it, so the project is right not to have it.
|
|
440
|
+
function checkDocs(actx, resolved, report) {
|
|
262
441
|
const fs = actx.fs();
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
442
|
+
const root = actx.folder;
|
|
443
|
+
const name = resolved.name;
|
|
444
|
+
const origname = resolved.origname;
|
|
445
|
+
const aliased = name !== origname;
|
|
446
|
+
const dest = (0, kind_1.kindTrees)('docs', name);
|
|
447
|
+
const from = (0, kind_1.kindTrees)('docs', origname);
|
|
448
|
+
const trees = dest.flatMap((tree, i) => {
|
|
449
|
+
const scaffold = node_path_1.default.join(resolved.folder, ...from[i].path.split('/'));
|
|
450
|
+
if (!fs.existsSync(scaffold)) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
const templated = 'template' === tree.replace;
|
|
454
|
+
return [{
|
|
455
|
+
project: node_path_1.default.join(root, ...tree.path.split('/')),
|
|
456
|
+
scaffold,
|
|
457
|
+
replace: templated ? (0, stdrep_1.templateReplacements)(actx.model, name) : {},
|
|
458
|
+
kind: templated ? 'edited' : 'forked',
|
|
459
|
+
rename: aliased && !templated ?
|
|
460
|
+
(rel) => (0, target_1.aliasCmpName)(rel, origname, name) : undefined,
|
|
461
|
+
rewrite: aliased && !templated ?
|
|
462
|
+
(src) => (0, target_1.aliasCmpText)(src, origname, name) : undefined,
|
|
463
|
+
}];
|
|
464
|
+
});
|
|
465
|
+
compareTrees(actx, report, trees);
|
|
466
|
+
}
|
|
467
|
+
// Per-target source that a FEATURE package supplies for this target, as
|
|
468
|
+
// `project-relative path -> the file it was copied from`.
|
|
469
|
+
//
|
|
470
|
+
// Only for features whose source folder is not the target's own: everything
|
|
471
|
+
// the target ships for itself is already in the tree being walked, and
|
|
472
|
+
// listing it twice would compare it against itself.
|
|
473
|
+
//
|
|
474
|
+
// Only for ACTIVE features, because that is what `feature add` copies. A
|
|
475
|
+
// feature switched off later leaves its files behind, and those really are
|
|
476
|
+
// stale — reporting them is the point.
|
|
477
|
+
//
|
|
478
|
+
// Discovered with `findFeatureSources`, the same function the fan-out and the
|
|
479
|
+
// trim use, so doctor's idea of which files belong to a feature cannot drift
|
|
480
|
+
// from the one that put them there.
|
|
481
|
+
function foreignFeatureSource(actx, target) {
|
|
482
|
+
const out = new Map();
|
|
483
|
+
const features = actx.model?.main?.[types_1.KIT]?.feature ?? {};
|
|
484
|
+
for (const fname of Object.keys(features).sort()) {
|
|
485
|
+
if (false === features[fname]?.active) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const source = resolveDeclared('feature', fname, actx);
|
|
489
|
+
if (null == source) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
for (const [rel, from] of overlayFiles(actx, source, target)) {
|
|
493
|
+
out.set(rel, from);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
// One feature's per-target source, as `tm-relative path -> the file it was
|
|
499
|
+
// copied from`, for source the FEATURE package supplies rather than the
|
|
500
|
+
// target.
|
|
501
|
+
//
|
|
502
|
+
// ONE enumerator, used from both directions: the target walk needs these to
|
|
503
|
+
// know they are expected, and the feature check needs them to compare. Two
|
|
504
|
+
// enumerations would be two ideas of which files a feature owns, and the
|
|
505
|
+
// pair would drift — which is the failure this codebase keeps producing.
|
|
506
|
+
//
|
|
507
|
+
// Empty when the feature's source IS the target's own tree: those files are
|
|
508
|
+
// already in the tree being walked, and listing them would compare them
|
|
509
|
+
// against themselves.
|
|
510
|
+
function overlayFiles(actx, feature, target) {
|
|
511
|
+
const fs = actx.fs();
|
|
512
|
+
const out = new Map();
|
|
513
|
+
if (feature.folder === target.folder) {
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
// The feature package's overlay for THIS target, under the name the target
|
|
517
|
+
// has in its own source — an aliased target's templates live at
|
|
518
|
+
// `tm/<origname>`.
|
|
519
|
+
const overlay = node_path_1.default.join(feature.folder, 'tm', target.origname);
|
|
520
|
+
for (const found of (0, featureSource_1.findFeatureSources)(fs, overlay, [feature.name])) {
|
|
521
|
+
const from = node_path_1.default.join(overlay, found.path);
|
|
522
|
+
// A folder source is the whole feature directory; expand it, because the
|
|
523
|
+
// comparison is per file.
|
|
524
|
+
if (found.folder) {
|
|
525
|
+
for (const rel of walk(fs, from)) {
|
|
526
|
+
out.set(found.path + '/' + rel, node_path_1.default.join(from, rel));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
out.set(found.path, from);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
// A FEATURE's per-target source, checked from the feature's side.
|
|
536
|
+
//
|
|
537
|
+
// This is where those files are COMPARED. The target walk only marks them
|
|
538
|
+
// expected, so that each is compared exactly once — by the feature that owns
|
|
539
|
+
// it, which is also the only scope in which `feature add` will rewrite it.
|
|
540
|
+
//
|
|
541
|
+
// Doing it the other way round — leaving the comparison in the target walk —
|
|
542
|
+
// left the gate open in the case it was built for: `package update` on a
|
|
543
|
+
// FEATURE package scopes doctor to that feature, no target is walked, and so
|
|
544
|
+
// nothing compared the very overlay the feature package supplies. Verified
|
|
545
|
+
// before fixing: the update succeeded without asking, and the local edit was
|
|
546
|
+
// destroyed.
|
|
547
|
+
function checkFeatureSource(actx, feature, targets, report) {
|
|
548
|
+
const fs = actx.fs();
|
|
549
|
+
const root = actx.folder;
|
|
550
|
+
const model = actx.model;
|
|
551
|
+
for (const [tname, target] of targets) {
|
|
552
|
+
for (const [rel, from] of overlayFiles(actx, feature, target)) {
|
|
553
|
+
const project = node_path_1.default.join(root, 'tm', tname, rel);
|
|
554
|
+
const label = 'tm/' + tname + '/' + rel;
|
|
555
|
+
if (!fs.existsSync(project)) {
|
|
556
|
+
report.missing.push(label);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
// The same map `feature add` copies with — see helpers/stdrep. A
|
|
560
|
+
// different one here would report every substituted file as edited.
|
|
561
|
+
if (differs(fs, from, project, model, (0, stdrep_1.templateReplacements)(model, tname))) {
|
|
562
|
+
report.edited.push(label);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// The copied MODEL FILE — `model/<kind>/<name>.aontu`, written by add with
|
|
568
|
+
// the `'BASE'` replacement, and overwritten on every resync exactly as
|
|
569
|
+
// `src/cmp` and `tm` are.
|
|
570
|
+
//
|
|
571
|
+
// It is not scaffolding trivia. A target's carries its dependency set, its
|
|
572
|
+
// `phase` gates, `srcfeature`, `feature.trim` / `feature.fullset` and its
|
|
573
|
+
// publish-registry identity; a feature's carries its version, `active`
|
|
574
|
+
// default, `config` defaults and hook wiring. So a maintainer who fixes a dep
|
|
575
|
+
// version there loses the fix on the next resync with nothing said (the SDK
|
|
576
|
+
// regresses with nobody touching it, which is the failure the "a project
|
|
577
|
+
// decision belongs in the MODEL" rule exists to prevent), and a project whose
|
|
578
|
+
// copy predates a scaffold change carries the old declaration forever while
|
|
579
|
+
// doctor reports it in sync.
|
|
580
|
+
//
|
|
581
|
+
// KIND-NEUTRAL, because the file is the same kind of thing for every kind:
|
|
582
|
+
// one definition, copied, stamped with provenance, optionally renamed for an
|
|
583
|
+
// alias. Only the alias rewrite is kind-specific, and the registry already
|
|
584
|
+
// says which kinds can be aliased at all.
|
|
585
|
+
function checkItemModel(actx, kind, source, report) {
|
|
586
|
+
const name = source.name;
|
|
587
|
+
const origname = source.origname;
|
|
588
|
+
const base = source.base;
|
|
589
|
+
const fs = actx.fs();
|
|
590
|
+
const scaffold = (0, definition_1.definitionPath)(source.folder, kind, origname);
|
|
591
|
+
// Nothing to compare against — a source that no longer ships this item.
|
|
271
592
|
if (!fs.existsSync(scaffold)) {
|
|
272
593
|
return;
|
|
273
594
|
}
|
|
274
|
-
|
|
275
|
-
|
|
595
|
+
// An ALIAS's model file is PROJECT-OWNED: `target add go~go2` creates it and
|
|
596
|
+
// never overwrites it again, because differentiating it is the whole point
|
|
597
|
+
// of an alias (a second Go module needs its own module name and deps), and
|
|
598
|
+
// add-a-target tells the project to edit it. So a difference here is not a
|
|
599
|
+
// fork — `target add` will not revert it — and must not fail the check.
|
|
600
|
+
//
|
|
601
|
+
// It is still worth REPORTING, which it never was before: the origin was
|
|
602
|
+
// unrecoverable, so doctor skipped the file entirely and an alias that had
|
|
603
|
+
// drifted far from a moved-on upstream said nothing. With `origname`
|
|
604
|
+
// recorded the comparison is possible, so it runs, with the same key
|
|
605
|
+
// rewrite the add applied — and anything left over is the project's own
|
|
606
|
+
// differentiation, reported as informational.
|
|
607
|
+
//
|
|
608
|
+
// Only for a kind that CAN be aliased. A feature cannot, so for features
|
|
609
|
+
// this is always false and the ownership argument never applies — the
|
|
610
|
+
// registry says which, rather than this inferring it from the names
|
|
611
|
+
// happening to match.
|
|
612
|
+
const aliased = (0, kind_1.kindDef)(kind).alias && name !== origname;
|
|
613
|
+
const project = (0, definition_1.definitionPath)(actx.folder, kind, name);
|
|
614
|
+
const label = 'model/' + kind + '/' + name + '.aontu';
|
|
276
615
|
if (!fs.existsSync(project)) {
|
|
277
616
|
report.missing.push(label);
|
|
278
617
|
return;
|
|
279
618
|
}
|
|
280
|
-
// The substitution
|
|
619
|
+
// The substitution the add applied on the way in: jostraca's template()
|
|
281
620
|
// against the model (so `module: name: '$$name$$'` arrives as the project
|
|
282
|
-
// slug) plus the
|
|
621
|
+
// slug) plus the provenance block recording where the scaffold was.
|
|
283
622
|
//
|
|
284
623
|
// NOT templateReplacements() — that is the tm/ tree's map. The two writers
|
|
285
|
-
// pass different maps, so doctor has to as well, or every
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
624
|
+
// pass different maps, so doctor has to as well, or every project reads as
|
|
625
|
+
// forked on the `base:` line alone. The map itself is shared with the
|
|
626
|
+
// writer (helpers/stdrep) so the two cannot drift.
|
|
627
|
+
//
|
|
628
|
+
// `package` comes from the SOURCE's manifest, not from what the copy
|
|
629
|
+
// records, for the same reason `base` does: what this compares is what the
|
|
630
|
+
// add would write NOW. A package that renamed itself therefore reads as
|
|
631
|
+
// forked, which is accurate — the next add would rewrite the line.
|
|
632
|
+
const provenance = (0, stdrep_1.provenanceReplace)({ base, origname, name, package: source.package });
|
|
633
|
+
// For an alias, compare against what the origin WOULD produce under the new
|
|
634
|
+
// name, so the rename itself is not the difference.
|
|
635
|
+
const rename = (0, kind_1.kindDef)(kind).rename;
|
|
636
|
+
const rewrite = (aliased && null != rename) ?
|
|
637
|
+
(src) => rename(src, origname, name) : undefined;
|
|
638
|
+
if (!differs(fs, scaffold, project, actx.model, provenance, undefined, rewrite)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (aliased) {
|
|
642
|
+
report.aliasedDiff.push(label);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// A copy written before the toolchain stamped some provenance key differs
|
|
646
|
+
// from the scaffold by exactly the lines carrying that key. That is not a
|
|
647
|
+
// fork — the project changed nothing — and reporting it as one would turn
|
|
648
|
+
// every existing consumer's CI red on upgrade.
|
|
649
|
+
//
|
|
650
|
+
// PER KEY, not all-or-nothing. The keys arrived in stages — `base` and
|
|
651
|
+
// `origname` first, `package` with the manifest — so a project that
|
|
652
|
+
// resynced between two of them holds a copy that IS stamped (it has `base`)
|
|
653
|
+
// and is still missing a later key. An all-or-nothing "is it stamped at
|
|
654
|
+
// all" test called every one of those a fork: on the released 3.4.8
|
|
655
|
+
// scaffold only ts, csharp and swift carried the anchor, and `ts` is in
|
|
656
|
+
// essentially every consumer SDK, so that was close to the whole installed
|
|
657
|
+
// base going red on a file nobody touched.
|
|
658
|
+
if (stampOnly(fs, scaffold, project, actx.model, provenance, rewrite)) {
|
|
659
|
+
report.resyncPending.push(label);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
report.forked.push(label);
|
|
663
|
+
}
|
|
664
|
+
// Is the ONLY difference a provenance line the copy has not been given yet?
|
|
665
|
+
//
|
|
666
|
+
// MATCHED AGAINST THE EXACT LINES THE STAMP PRODUCES, never against a pattern
|
|
667
|
+
// for "a line that looks like provenance". `base`, `origname` and `package`
|
|
668
|
+
// are not reserved words: `main: kit: target: <t>: module: package` (the Go
|
|
669
|
+
// root package identifier) and `publish: registry: package` (the published
|
|
670
|
+
// package name) are declared model slots that a target model may write in
|
|
671
|
+
// block form, on their own line, looking exactly like a provenance line to
|
|
672
|
+
// any regex. A generic pattern got BOTH directions wrong on such a model —
|
|
673
|
+
// it read the copy's `module: package:` as proof the copy was already
|
|
674
|
+
// stamped, so an untouched pre-manifest file was reported as forked; and it
|
|
675
|
+
// stripped that same real line from the comparison, so DELETING it — an
|
|
676
|
+
// actual fork, silently reverted by the next `target add` — was reported as
|
|
677
|
+
// a pending resync and passed the check.
|
|
678
|
+
//
|
|
679
|
+
// Comparing against `provenanceReplace`'s own output has no such ambiguity,
|
|
680
|
+
// and keeps the reader tied to the writer, which is the whole point of
|
|
681
|
+
// helpers/stdrep.
|
|
682
|
+
//
|
|
683
|
+
// The rule, once the two are known to differ:
|
|
684
|
+
//
|
|
685
|
+
// - every line the EXPECTED has and the copy lacks must be a stamp line;
|
|
686
|
+
// - the copy must have NO line the expected lacks.
|
|
687
|
+
//
|
|
688
|
+
// The second half is what keeps the tolerance narrow, as the previous
|
|
689
|
+
// rollout's review required: a changed `package:` value leaves the old line
|
|
690
|
+
// in the copy and unmatched, so it is a fork and is reported as one.
|
|
691
|
+
function stampOnly(fs, scaffoldPath, projectPath, model, provenance, rewrite) {
|
|
692
|
+
const { expected, actual } = renderPair(fs, scaffoldPath, projectPath, model, provenance, rewrite);
|
|
693
|
+
// The rendered block, as lines: `base: '...'` plus whichever of
|
|
694
|
+
// `origname:` / `package:` applied. Trimmed on both sides of the
|
|
695
|
+
// comparison, because the anchor's own indentation belongs to the scaffold.
|
|
696
|
+
const stamp = new Set(Object.values(provenance).join('\n').split('\n')
|
|
697
|
+
.map((s) => s.trim()));
|
|
698
|
+
const onlyExpected = lineDiff(expected, actual);
|
|
699
|
+
const onlyActual = lineDiff(actual, expected);
|
|
700
|
+
return 0 === onlyActual.length &&
|
|
701
|
+
0 < onlyExpected.length &&
|
|
702
|
+
onlyExpected.every((line) => stamp.has(line.trim()));
|
|
703
|
+
}
|
|
704
|
+
// Lines of `a` that `b` does not have, counting duplicates.
|
|
705
|
+
function lineDiff(a, b) {
|
|
706
|
+
const pool = new Map();
|
|
707
|
+
for (const line of b.split('\n')) {
|
|
708
|
+
pool.set(line, (pool.get(line) ?? 0) + 1);
|
|
709
|
+
}
|
|
710
|
+
const out = [];
|
|
711
|
+
for (const line of a.split('\n')) {
|
|
712
|
+
const n = pool.get(line) ?? 0;
|
|
713
|
+
if (0 < n) {
|
|
714
|
+
pool.set(line, n - 1);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
out.push(line);
|
|
718
|
+
}
|
|
289
719
|
}
|
|
720
|
+
return out;
|
|
290
721
|
}
|
|
291
722
|
// Every file under `dir`, as forward-slash paths relative to it, sorted.
|
|
292
723
|
// Missing directory -> no files (a target that was never added).
|
|
@@ -298,7 +729,7 @@ function walk(fs, dir) {
|
|
|
298
729
|
const descend = (rel) => {
|
|
299
730
|
const abs = '' === rel ? dir : node_path_1.default.join(dir, rel);
|
|
300
731
|
for (const entry of fs.readdirSync(abs).sort()) {
|
|
301
|
-
if (
|
|
732
|
+
if (ignoredEntry(entry)) {
|
|
302
733
|
continue;
|
|
303
734
|
}
|
|
304
735
|
const entryrel = '' === rel ? entry : rel + '/' + entry;
|
|
@@ -324,22 +755,43 @@ function excluded(rel, excludes) {
|
|
|
324
755
|
// Compare a scaffold file with a project file, applying the SAME substitution
|
|
325
756
|
// `target add` applied on the way in. Without this the comparison reports
|
|
326
757
|
// every substituted placeholder as an edit.
|
|
327
|
-
|
|
758
|
+
// `ignore` drops matching lines from BOTH sides before comparing, for a
|
|
759
|
+
// difference that is known not to be a fork (see the provenance rollout in
|
|
760
|
+
// checkTargetModel). It is deliberately a second, narrower question asked
|
|
761
|
+
// only after a plain comparison has already found a difference — so a file
|
|
762
|
+
// that matches exactly never depends on it.
|
|
763
|
+
function differs(fs, scaffoldPath, projectPath, model, replace, ignore, rewrite) {
|
|
328
764
|
if (BINARY_RE.test(scaffoldPath)) {
|
|
329
765
|
return !fs.readFileSync(scaffoldPath).equals(fs.readFileSync(projectPath));
|
|
330
766
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
767
|
+
const { expected, actual } = renderPair(fs, scaffoldPath, projectPath, model, replace, rewrite);
|
|
768
|
+
if (null == ignore) {
|
|
769
|
+
return expected !== actual;
|
|
770
|
+
}
|
|
771
|
+
const strip = (s) => s.split('\n').filter((line) => !ignore(line)).join('\n');
|
|
772
|
+
return strip(expected) !== strip(actual);
|
|
773
|
+
}
|
|
774
|
+
// What `target add` WOULD write, beside what the project actually has.
|
|
775
|
+
//
|
|
776
|
+
// One definition, shared by the byte comparison and the stamp-only tolerance,
|
|
777
|
+
// so the two can never disagree about what the expected text is.
|
|
778
|
+
//
|
|
779
|
+
// template() runs even when there is nothing to REPLACE, because jostraca's
|
|
780
|
+
// Copy always interpolates `$$ref$$` against the model as well — the replace
|
|
781
|
+
// map is an extra, not the whole substitution. Skipping it for the src/cmp
|
|
782
|
+
// tree (whose Copy passes no replace map) meant the three Config fragments
|
|
783
|
+
// that carry `$$const.Name$$` / `$$main.kit.info.servers.0.url$$` arrived
|
|
784
|
+
// substituted and compared as bytes: every project with ts, js or dart
|
|
785
|
+
// reported `src/cmp/<t>/fragment/Config.fragment.<ext>` as FORKED straight
|
|
786
|
+
// out of `target add`, which is precisely the noise this exists to remove.
|
|
787
|
+
// With no `$$` refs and no replace keys, template() is the identity.
|
|
788
|
+
function renderPair(fs, scaffoldPath, projectPath, model, replace, rewrite) {
|
|
789
|
+
const rawsrc = fs.readFileSync(scaffoldPath, 'utf8');
|
|
790
|
+
const src = null == rewrite ? rawsrc : rewrite(rawsrc);
|
|
791
|
+
return {
|
|
792
|
+
expected: (0, jostraca_1.template)(src, model, { replace }),
|
|
793
|
+
actual: fs.readFileSync(projectPath, 'utf8'),
|
|
794
|
+
};
|
|
343
795
|
}
|
|
344
796
|
// trimFeatures logs its decisions; doctor is a report, not a run.
|
|
345
797
|
function quietLog(log) {
|