@ecoma-io/archkeep 0.17.0 → 0.18.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/cli.mjs CHANGED
@@ -931,12 +931,8 @@ async function runDiff(options, { cwd, env }) {
931
931
  *
932
932
  * `--capture` writes the evidence snapshot a later run compares against;
933
933
  * `delta <baseline>` loads one, re-judges both sides under the current law,
934
- * and folds the classification into the exit code — the one descriptive-family
935
- * verb beside `check` and `fitness` whose verdict carries exit 1
936
- * (`./src/commands/delta.mjs` owns the fold).
937
- *
938
934
  * @param {{format: string, output: string|null, config: string|null, capture: boolean,
939
- * paths: string[]}} options
935
+ * eventOut: string|null, paths: string[]}} options
940
936
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
941
937
  * @returns {Promise<number>}
942
938
  */
@@ -948,6 +944,13 @@ async function runDelta(options, { cwd, env }) {
948
944
  );
949
945
  return EXIT.usage;
950
946
  }
947
+ if (options.eventOut !== null) {
948
+ env.err(
949
+ `archkeep: delta --capture does not take --event-out — an event records a transition, ` +
950
+ `and a capture is one side of it`,
951
+ );
952
+ return EXIT.usage;
953
+ }
951
954
  } else if (options.paths.length !== 1) {
952
955
  env.err(
953
956
  `archkeep: delta takes exactly one positional argument (the baseline evidence snapshot), ` +
@@ -972,7 +975,6 @@ async function runDelta(options, { cwd, env }) {
972
975
  const { text } = captureDelta(commandContext, { config });
973
976
  if (options.output) {
974
977
  // Atomic, symlink-safe write — `writeOutputReport`'s own docstring
975
- // owns the mechanism and the threat it closes.
976
978
  if (!writeOutputReport(options.output, text, env, cwd, options.config)) return EXIT.error;
977
979
  env.err(`archkeep: delta baseline captured → ${options.output}`);
978
980
  } else {
@@ -985,7 +987,10 @@ async function runDelta(options, { cwd, env }) {
985
987
  const baselinePath = isAbsolute(options.paths[0])
986
988
  ? resolve(options.paths[0])
987
989
  : resolve(cwd, options.paths[0]);
988
- result = await deltaCommand(baselinePath, commandContext, { config });
990
+ result = await deltaCommand(baselinePath, commandContext, {
991
+ config,
992
+ eventOut: options.eventOut,
993
+ });
989
994
  } catch (error) {
990
995
  const usageError = error instanceof UsageError;
991
996
  env.err(String(error?.message ?? error));
@@ -1009,6 +1014,18 @@ async function runDelta(options, { cwd, env }) {
1009
1014
  env.out(report);
1010
1015
  }
1011
1016
 
1017
+ // The event the run recorded, when `--event-out` was given — the store's
1018
+ // own `duplicate` answer, so a rerun over the same transition says so
1019
+ // instead of implying a second event was appended. Capture mode refuses the
1020
+ // flag upstream; this line runs only for a compare.
1021
+ if (result.eventWrite !== null) {
1022
+ env.err(
1023
+ `archkeep: evolution event ${
1024
+ result.eventWrite.duplicate ? "duplicate, already recorded" : "recorded"
1025
+ } → ${options.eventOut}`,
1026
+ );
1027
+ }
1028
+
1012
1029
  // The exit fold `deltaCommand` computed: a non-waived introduced violation
1013
1030
  // is a finding, an unclassifiable item is a no-verdict, anything else is
1014
1031
  // clean — mapped here the same way `fitness`'s status is.
@@ -1214,7 +1231,7 @@ async function runReconcile(options, { cwd, env }) {
1214
1231
  * `check` remains the authority on the law.
1215
1232
  *
1216
1233
  * @param {{format: string, output: string|null, config: string|null,
1217
- * intent: string|null, paths: string[]}} options
1234
+ * intent: string|null, eventOut?: string|null, paths: string[]}} options
1218
1235
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function,
1219
1236
  * listFiles?: Function}}} runContext
1220
1237
  * @returns {Promise<number>}
@@ -1271,7 +1288,19 @@ async function runChange(options, { cwd, env }) {
1271
1288
  // profile-aware the same way `check` is.
1272
1289
  const { config } = await resolvePolicy(options, commandContext, cwd);
1273
1290
 
1274
- result = await changeCommand(baselinePath, intentPath, commandContext, { config });
1291
+ // `--event-out` names the reconcile event store directory, resolved from
1292
+ // cwd like the other path flags; `undefined` when absent, so a run
1293
+ // without the flag writes no event and stays byte-identical.
1294
+ const eventOut =
1295
+ typeof options.eventOut === "string" && options.eventOut !== ""
1296
+ ? isAbsolute(options.eventOut)
1297
+ ? options.eventOut
1298
+ : resolve(cwd, options.eventOut)
1299
+ : undefined;
1300
+ result = await changeCommand(baselinePath, intentPath, commandContext, {
1301
+ config,
1302
+ ...(eventOut === undefined ? {} : { eventOut }),
1303
+ });
1275
1304
  } catch (error) {
1276
1305
  const usageError = error instanceof UsageError;
1277
1306
  env.err(String(error?.message ?? error));
@@ -1581,7 +1610,7 @@ async function runExplain(options, { cwd, env }) {
1581
1610
  * same as `check` and `explain`, because the answer depends on which boundary
1582
1611
  * law is in effect.
1583
1612
  *
1584
- * @param {{format: string, output: string|null, config: string|null, plan: boolean, paths: string[]}} options
1613
+ * @param {{format: string, output: string|null, config: string|null, plan: boolean, paths: string[], historyDir: string|null}} options
1585
1614
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
1586
1615
  * @returns {Promise<number>}
1587
1616
  */
@@ -1622,8 +1651,14 @@ async function runContextCommand(options, { cwd, env }) {
1622
1651
  // profile-aware the same way `check` is.
1623
1652
  const { config } = await resolvePolicy(options, commandContext, cwd);
1624
1653
 
1654
+ const historyDir = options.historyDir
1655
+ ? isAbsolute(options.historyDir)
1656
+ ? options.historyDir
1657
+ : resolve(cwd, options.historyDir)
1658
+ : null;
1659
+
1625
1660
  result = options.plan
1626
- ? await planContextCommand(projectName, scopePaths, commandContext, config)
1661
+ ? await planContextCommand(projectName, scopePaths, commandContext, config, historyDir)
1627
1662
  : contextCommand(projectName, commandContext, config);
1628
1663
  } catch (error) {
1629
1664
  const usageError = error instanceof UsageError;
@@ -2026,7 +2061,7 @@ async function runTrajectory(options, { cwd, env }) {
2026
2061
  * production both are undefined and every revision is read for real.
2027
2062
  *
2028
2063
  * @param {{format: string, output: string|null, base: string|null, head: string|null,
2029
- * paths: string[]}} options
2064
+ * eventOut: string|null, paths: string[]}} options
2030
2065
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
2031
2066
  * @returns {Promise<number>}
2032
2067
  */
@@ -2057,9 +2092,18 @@ async function runEvolution(options, { cwd, env }) {
2057
2092
 
2058
2093
  let result;
2059
2094
  try {
2095
+ // `--event-out` names the transition event store directory, resolved from
2096
+ // cwd like the other path flags; `null` when absent, so a run without the
2097
+ // flag writes no event and stays byte-identical.
2098
+ const eventOut =
2099
+ typeof options.eventOut === "string" && options.eventOut !== ""
2100
+ ? isAbsolute(options.eventOut)
2101
+ ? options.eventOut
2102
+ : resolve(cwd, options.eventOut)
2103
+ : null;
2060
2104
  result = await evolutionCommand(
2061
2105
  root,
2062
- { base: options.base, head: options.head },
2106
+ { base: options.base, head: options.head, eventOut },
2063
2107
  {
2064
2108
  readGraph: env.readGraph,
2065
2109
  listFiles: env.listFiles,
@@ -2098,7 +2142,7 @@ async function runEvolution(options, { cwd, env }) {
2098
2142
  * consumer-managed directory `history` reads, so a ledger ages across the
2099
2143
  * same snapshots the evolution record is built from.
2100
2144
  *
2101
- * @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
2145
+ * @param {{format: string, output: string|null, config: string|null, events: string|null, paths: string[]}} options
2102
2146
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
2103
2147
  * @returns {Promise<number>}
2104
2148
  */
@@ -2126,7 +2170,10 @@ async function runDebt(options, { cwd, env }) {
2126
2170
  // profile-selected workspace resolves the same way `check` does.
2127
2171
  const { config } = await resolvePolicy(options, commandContext, cwd);
2128
2172
 
2129
- result = await debtCommand(dir, commandContext, { config });
2173
+ result = await debtCommand(dir, commandContext, {
2174
+ config,
2175
+ events: options.events,
2176
+ });
2130
2177
  } catch (error) {
2131
2178
  const usageError = error instanceof UsageError;
2132
2179
  env.err(String(error?.message ?? error));
@@ -2530,6 +2577,17 @@ const DELTA_FLAG_HELP = Object.freeze([
2530
2577
  : `<workspace root>/${boundaryConfig}`,
2531
2578
  ]),
2532
2579
  }),
2580
+ Object.freeze({
2581
+ flag: "--event-out",
2582
+ key: "eventOut",
2583
+ arg: "<dir>",
2584
+ describe: Object.freeze([
2585
+ "Append the delta's evolution event to this directory",
2586
+ "(one canonical record per transition; idempotent — a",
2587
+ "rerun over the same transition writes nothing new).",
2588
+ "Absent: no event file is written",
2589
+ ]),
2590
+ }),
2533
2591
  ]);
2534
2592
 
2535
2593
  /**
@@ -2566,6 +2624,16 @@ const CHANGE_FLAG_HELP = Object.freeze([
2566
2624
  arg: "<file>",
2567
2625
  describe: Object.freeze(["Write the report to a file instead of stdout"]),
2568
2626
  }),
2627
+ Object.freeze({
2628
+ flag: "--event-out",
2629
+ key: "eventOut",
2630
+ arg: "<dir>",
2631
+ describe: Object.freeze([
2632
+ "Also write the reconcile EvolutionEvent to this directory",
2633
+ "(one file per run, idempotent; the classification always",
2634
+ "rides the envelope result — see docs/concepts/evolution.md)",
2635
+ ]),
2636
+ }),
2569
2637
  Object.freeze({
2570
2638
  flag: "--config",
2571
2639
  key: "config",
@@ -2839,6 +2907,16 @@ const EVOLUTION_FLAG_HELP = Object.freeze([
2839
2907
  "linear descendant of --base with no merges between",
2840
2908
  ]),
2841
2909
  }),
2910
+ Object.freeze({
2911
+ flag: "--event-out",
2912
+ key: "eventOut",
2913
+ arg: "<dir>",
2914
+ describe: Object.freeze([
2915
+ "Append one EvolutionEvent per revision pair to this directory",
2916
+ "(idempotent — a re-run over the same pair records a duplicate, never",
2917
+ "a second event). docs/concepts/evolution.md owns the event model.",
2918
+ ]),
2919
+ }),
2842
2920
  ]);
2843
2921
 
2844
2922
  /**
@@ -2981,6 +3059,15 @@ const DEBT_FLAG_HELP = Object.freeze([
2981
3059
  : `<workspace root>/${boundaryConfig}`,
2982
3060
  ]),
2983
3061
  }),
3062
+ Object.freeze({
3063
+ flag: "--events",
3064
+ key: "events",
3065
+ arg: "<dir>",
3066
+ describe: Object.freeze([
3067
+ "Link the evolution event store in <dir> so debt entries carry",
3068
+ "introducedBy/resolvedBy refs and a resolved list",
3069
+ ]),
3070
+ }),
2984
3071
  ]);
2985
3072
 
2986
3073
  /**
@@ -3139,6 +3226,18 @@ const CONTEXT_FLAG_HELP = Object.freeze([
3139
3226
  : `<workspace root>/${boundaryConfig}`,
3140
3227
  ]),
3141
3228
  }),
3229
+ Object.freeze({
3230
+ flag: "--history-dir",
3231
+ key: "historyDir",
3232
+ arg: "<dir>",
3233
+ describe: Object.freeze([
3234
+ "Path to the workspace's history directory. When given and",
3235
+ "the directory holds archived snapshots, the planning context",
3236
+ "includes the architecture-debt snapshot: current violations,",
3237
+ "exemptions, and gaps aged across the history. Used only with",
3238
+ "`--plan`; ignored otherwise.",
3239
+ ]),
3240
+ }),
3142
3241
  ]);
3143
3242
 
3144
3243
  /**
@@ -3252,7 +3351,13 @@ const COMMANDS = Object.freeze({
3252
3351
  summary: "Classify how boundary violations moved between a captured baseline and head",
3253
3352
  flagHelp: DELTA_FLAG_HELP,
3254
3353
  flags: Object.freeze(Object.fromEntries(DELTA_FLAG_HELP.map((f) => [f.flag, f.key]))),
3255
- defaults: Object.freeze({ format: "text", output: null, config: null, capture: false }),
3354
+ defaults: Object.freeze({
3355
+ format: "text",
3356
+ output: null,
3357
+ config: null,
3358
+ capture: false,
3359
+ eventOut: null,
3360
+ }),
3256
3361
  formats: DELTA_FORMATS,
3257
3362
  booleans: Object.freeze(["capture"]),
3258
3363
  run: runDelta,
@@ -3263,7 +3368,13 @@ const COMMANDS = Object.freeze({
3263
3368
  summary: "Reconcile a declared change intent against the architectural delta",
3264
3369
  flagHelp: CHANGE_FLAG_HELP,
3265
3370
  flags: Object.freeze(Object.fromEntries(CHANGE_FLAG_HELP.map((f) => [f.flag, f.key]))),
3266
- defaults: Object.freeze({ format: "text", output: null, config: null, intent: null }),
3371
+ defaults: Object.freeze({
3372
+ format: "text",
3373
+ output: null,
3374
+ config: null,
3375
+ intent: null,
3376
+ eventOut: null,
3377
+ }),
3267
3378
  formats: DESCRIBABLE_FORMATS,
3268
3379
  run: runChange,
3269
3380
  }),
@@ -3346,7 +3457,13 @@ const COMMANDS = Object.freeze({
3346
3457
  summary: "Describe how the architecture evolved across a Git revision range",
3347
3458
  flagHelp: EVOLUTION_FLAG_HELP,
3348
3459
  flags: Object.freeze(Object.fromEntries(EVOLUTION_FLAG_HELP.map((f) => [f.flag, f.key]))),
3349
- defaults: Object.freeze({ format: "text", output: null, base: null, head: null }),
3460
+ defaults: Object.freeze({
3461
+ format: "text",
3462
+ output: null,
3463
+ base: null,
3464
+ head: null,
3465
+ eventOut: null,
3466
+ }),
3350
3467
  formats: DESCRIBABLE_FORMATS,
3351
3468
  run: runEvolution,
3352
3469
  }),
@@ -3376,7 +3493,7 @@ const COMMANDS = Object.freeze({
3376
3493
  summary: "Print the architecture-debt ledger across snapshots",
3377
3494
  flagHelp: DEBT_FLAG_HELP,
3378
3495
  flags: Object.freeze(Object.fromEntries(DEBT_FLAG_HELP.map((f) => [f.flag, f.key]))),
3379
- defaults: Object.freeze({ format: "text", output: null, config: null }),
3496
+ defaults: Object.freeze({ format: "text", output: null, config: null, events: null }),
3380
3497
  formats: DESCRIBABLE_FORMATS,
3381
3498
  run: runDebt,
3382
3499
  }),
@@ -3406,7 +3523,13 @@ const COMMANDS = Object.freeze({
3406
3523
  summary: "Show the architecture constraints that apply to a project",
3407
3524
  flagHelp: CONTEXT_FLAG_HELP,
3408
3525
  flags: Object.freeze(Object.fromEntries(CONTEXT_FLAG_HELP.map((f) => [f.flag, f.key]))),
3409
- defaults: Object.freeze({ format: "text", output: null, config: null, plan: false }),
3526
+ defaults: Object.freeze({
3527
+ format: "text",
3528
+ output: null,
3529
+ config: null,
3530
+ plan: false,
3531
+ historyDir: null,
3532
+ }),
3410
3533
  formats: DESCRIBABLE_FORMATS,
3411
3534
  booleans: Object.freeze(["plan"]),
3412
3535
  run: runContextCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecoma-io/archkeep",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript, Vue, Java and Kotlin",
5
5
  "keywords": [
6
6
  "architecture",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@nx/eslint-plugin": ">=21",
56
- "fast-xml-parser": "5.11.0",
56
+ "fast-xml-parser": "5.11.1",
57
57
  "nx": ">=21",
58
58
  "typescript": ">=5 <7",
59
59
  "vue": ">=3"
@@ -168,12 +168,15 @@ function codeDependencies(graph) {
168
168
  * @param {object} intent The normalized model from `./model.mjs`.
169
169
  * @param {{nodes: object, dependencies?: object}} graph
170
170
  * @returns {{verdict: "ok"|"findings"|"no-verdict",
171
- * findings: object[], unresolved: object[], boundaries: object[], notes: string[]}}
171
+ * findings: object[], unresolved: object[], boundaries: object[], notes: string[],
172
+ * gaps: {from: string, to: string, note: string}[]}}
172
173
  * `findings` are `{source, target, rule, boundaryFrom, boundaryTo, message}`;
173
174
  * `unresolved` are `{boundary, issue}` for every empty side or empty
174
175
  * boundary; `boundaries` are `{name, projects[]}` (sorted members); `notes`
175
176
  * are coverage notes that change no verdict — today only an
176
- * `"optional": true` `allowed` row whose statement is not yet built.
177
+ * `"optional": true` `allowed` row whose statement is not yet built; `gaps`
178
+ * carry the same rows' structured `{from, to}` identities (F-DEB-8), the
179
+ * stable key the debt ledger hashes an aspirational-gap entry by.
177
180
  */
178
181
  export function judgeIntent(intent, graph) {
179
182
  const nodes = graph.nodes ?? {};
@@ -191,6 +194,7 @@ export function judgeIntent(intent, graph) {
191
194
  const findings = [];
192
195
  const unresolved = [];
193
196
  const notes = [];
197
+ const gaps = [];
194
198
 
195
199
  for (const boundary of boundaries) {
196
200
  if (boundary.projects.length === 0) {
@@ -291,9 +295,18 @@ export function judgeIntent(intent, graph) {
291
295
  // Absence tolerated — aspirational, not drift — but it is still a
292
296
  // coverage note and the caller threads it into the report's coverage
293
297
  // notes, so a reader can tell "optional and absent" from "never checked".
294
- notes.push(
295
- `optional allowed intent "${row.from}" → "${row.to}" is not yet observed — aspirational, not drift`,
296
- );
298
+ const note = `optional allowed intent "${row.from}" → "${row.to}" is not yet observed — aspirational, not drift`;
299
+ notes.push(note);
300
+ // The structured `{from, to}` is the STABLE identity of the gap — the
301
+ // debt ledger keys an aspirational-gap entry by it (F-DEB-8), never by
302
+ // the prose note, so a reworded note does not re-key the fact.
303
+ gaps.push({
304
+ from: row.from,
305
+ to: row.to,
306
+ note,
307
+ boundaryFrom: from.boundaryName ?? row.from,
308
+ boundaryTo: to.boundaryName ?? row.to,
309
+ });
297
310
  return;
298
311
  }
299
312
  const pairs = [];
@@ -535,5 +548,5 @@ export function judgeIntent(intent, graph) {
535
548
 
536
549
  const verdict = findings.length > 0 ? "findings" : unresolved.length > 0 ? "no-verdict" : "ok";
537
550
 
538
- return { verdict, findings, unresolved, boundaries, notes };
551
+ return { verdict, findings, unresolved, boundaries, notes, gaps };
539
552
  }
@@ -313,6 +313,43 @@ export function findChangeIntentViolations(raw) {
313
313
 
314
314
  return violations;
315
315
  }
316
+ /**
317
+ * The breadth guard (wave 3, design §5): everything wrong with a NORMALIZED
318
+ * intent whose declaration is empty while its own prose asserts a change.
319
+ *
320
+ * An intent with zero declared rows — no project rows, no edge rows, no
321
+ * declared constraints — declares that NOTHING material will change. Its
322
+ * `summary`, if present, is the author's own statement of what the change
323
+ * does; when the rows are empty that statement is a claim nothing in the
324
+ * declaration can be verified against, and the reconciliation would read
325
+ * `matched` over an unchanged tree while the author's prose says the tree
326
+ * moved. Prose cannot assert what rows must state: this is the one bypass
327
+ * around the grammar's reject-by-name discipline, and it is refused loudly
328
+ * (`parseChangeIntent` throws, exit 3 upstream) instead of reconciling.
329
+ *
330
+ * The rule is deliberately stricter than "empty projects/edges": it also
331
+ * requires the constraints section empty. A declared constraint IS a row —
332
+ * `noNewViolations: true` states a verifiable promise, and an intent that
333
+ * declares one is not a catch-all no matter what its summary says.
334
+ *
335
+ * @param {object} intent A `parseChangeIntent` result — the normalized shape,
336
+ * with absent raw sections already normalized to empty arrays.
337
+ * @returns {string[]} Messages; empty when the intent is not a catch-all.
338
+ */
339
+ export function findChangeIntentBreadthViolations(intent) {
340
+ const declaredRows =
341
+ intent.projects.add.length +
342
+ intent.projects.remove.length +
343
+ intent.edges.add.length +
344
+ intent.edges.remove.length +
345
+ Object.keys(intent.constraints).length;
346
+ if (declaredRows > 0 || intent.summary === undefined) return [];
347
+ return [
348
+ "summary: the intent declares no rows (no projects, no edges, no constraints) while its " +
349
+ "summary asserts a material change — prose cannot assert what rows must state; declare " +
350
+ "the material consequences in the rows, or drop the summary",
351
+ ];
352
+ }
316
353
 
317
354
  /**
318
355
  * Parses and validates change-intent text into the normalized shape the
@@ -346,16 +383,18 @@ export function parseChangeIntent(text, path) {
346
383
  { cause },
347
384
  );
348
385
  }
386
+ // Shape validation first, then the breadth guard over the NORMALIZED
387
+ // intent — the guard's subject is the intent as the command will consume
388
+ // it, with absent sections already normalized to empty expectations. Both
389
+ // name every violation at once in the one throw below, so a malformed
390
+ // catch-all is reported whole rather than piecemeal.
349
391
  const violations = findChangeIntentViolations(parsed);
350
- if (violations.length > 0) {
351
- throw new Error(
352
- `archkeep: the change intent '${path}' is not a usable contract:\n ` +
353
- violations.join("\n "),
354
- );
355
- }
356
- return {
392
+ const intent = {
357
393
  version: parsed.version,
358
- base: { commit: parsed.base.commit },
394
+ // Defensive access: an invalid `base` is caught by the shape violations
395
+ // below and thrown before this object is ever returned, so a missing
396
+ // section must not crash the normalization itself.
397
+ base: { commit: parsed.base?.commit },
359
398
  ...(parsed.summary === undefined ? {} : { summary: parsed.summary }),
360
399
  projects: {
361
400
  add: parsed.projects?.add ?? [],
@@ -367,6 +406,14 @@ export function parseChangeIntent(text, path) {
367
406
  },
368
407
  constraints: { ...(parsed.constraints ?? {}) },
369
408
  };
409
+ violations.push(...findChangeIntentBreadthViolations(intent));
410
+ if (violations.length > 0) {
411
+ throw new Error(
412
+ `archkeep: the change intent '${path}' is not a usable contract:\n ` +
413
+ violations.join("\n "),
414
+ );
415
+ }
416
+ return intent;
370
417
  }
371
418
 
372
419
  /**