rcf-lite 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/rcf.js +6 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/harness-template.md +11 -0
  5. package/guidance/managed/agent-instructions-block.hash +1 -1
  6. package/guidance/managed/agent-instructions-block.md +11 -0
  7. package/package.json +5 -3
  8. package/rcf/adrs/adr-010.json +30 -0
  9. package/rcf/code-nodes/cn-058.json +18 -0
  10. package/rcf/code-nodes/cn-059.json +14 -0
  11. package/rcf/code-nodes/cn-060.json +14 -0
  12. package/rcf/code-nodes/cn-061.json +14 -0
  13. package/rcf/code-nodes/cn-062.json +14 -0
  14. package/rcf/code-nodes/cn-063.json +15 -0
  15. package/rcf/code-nodes/cn-064.json +15 -0
  16. package/rcf/code-nodes/cn-065.json +16 -0
  17. package/rcf/code-nodes/cn-066.json +14 -0
  18. package/rcf/code-nodes/cn-067.json +15 -0
  19. package/rcf/code-nodes/cn-068.json +15 -0
  20. package/rcf/code-nodes/cn-069.json +16 -0
  21. package/rcf/fbs/fbs-016.json +39 -0
  22. package/rcf/fbs/fbs-017.json +40 -0
  23. package/rcf/fbs/fbs-018.json +34 -0
  24. package/rcf/fbs/fbs-019.json +33 -0
  25. package/rcf/requirements/req-010.json +20 -0
  26. package/rcf/test-suites/ts-026.json +54 -0
  27. package/rcf/test-suites/ts-027.json +115 -0
  28. package/rcf/test-suites/ts-028.json +46 -0
  29. package/rcf/test-suites/ts-029.json +46 -0
  30. package/rcf/user-stories/us-1001.json +56 -0
  31. package/rcf/user-stories/us-1002.json +96 -0
  32. package/rcf/user-stories/us-1003.json +48 -0
  33. package/rcf/user-stories/us-1004.json +48 -0
  34. package/src/admissibility/enforce.js +142 -0
  35. package/src/admissibility/index.js +8 -0
  36. package/src/admissibility/markers.js +104 -0
  37. package/src/admissibility/scope-lint.js +163 -0
  38. package/src/blueprint/apply.js +464 -0
  39. package/src/blueprint/conflicts.js +351 -0
  40. package/src/blueprint/diff.js +82 -0
  41. package/src/blueprint/index.js +12 -0
  42. package/src/blueprint/list.js +21 -0
  43. package/src/blueprint/loader.js +163 -0
  44. package/src/blueprint/manifest-writer.js +49 -0
  45. package/src/blueprint/namespace.js +145 -0
  46. package/src/blueprint/remove.js +105 -0
  47. package/src/blueprint/resolutions.js +83 -0
  48. package/src/blueprint/standards.js +148 -0
  49. package/src/blueprint/supersede.js +318 -0
  50. package/src/browser-verify/invariants.js +33 -6
  51. package/src/build/bundle.js +34 -11
  52. package/src/build/standards-selector.js +52 -0
  53. package/src/cli/blueprint.js +325 -0
  54. package/src/cli/create.js +49 -1
  55. package/src/cli/help.js +8 -0
  56. package/src/cli/init.js +20 -5
  57. package/src/cli/read.js +7 -1
  58. package/src/cli/standards.js +127 -0
  59. package/src/cli/test-suite.js +7 -2
  60. package/src/core/store/ids.js +168 -18
  61. package/src/core/store/loader.js +31 -17
  62. package/src/core/store/walker.js +62 -4
  63. package/src/core/store/writer.js +41 -11
  64. package/src/deployment/index.js +13 -0
  65. package/src/deployment/placeholder-detector.js +113 -0
  66. package/src/finalise/detect.js +51 -29
  67. package/src/finalise/index.js +16 -2
  68. package/src/finalise/ingest.js +41 -0
  69. package/src/mcp/tools.js +10 -2
  70. package/src/query/formatters/table.js +7 -10
  71. package/src/query/index.js +4 -0
  72. package/src/query/refuse-on-admissibility.js +73 -0
  73. package/src/query/trace.js +45 -4
  74. package/src/ruleset/index.js +140 -0
  75. package/src/ruleset/ruleset.json +146 -0
  76. package/src/verify/chain/index.js +31 -0
  77. package/src/verify/verdict/index.js +67 -0
@@ -0,0 +1,142 @@
1
+ // Chain-admissibility gate (NV-BL-ADM-05, NV-BL-ADM-06).
2
+ //
3
+ // Refuse-first per ratified ruling-sheet item 1 (2026-08-11): a chain
4
+ // that fails any admissibility check is refused before build starts.
5
+ // Explicit overrides are permitted for non-source-marker checks,
6
+ // recorded in the chain with rule id, reason, and authoring identity,
7
+ // and surfaced in the finalise summary. Source-comment markers
8
+ // (NV-BL-ADM-04) fall outside this generic override channel and are
9
+ // governed by the narrower ADR-only mechanism per ruling-sheet
10
+ // item 16 -- callers apply that check separately.
11
+ //
12
+ // NV-BL-ADM-06 (build-stage refusal on ruleset-version drift):
13
+ // composes with the scope-tag scans below so a build-time entry point
14
+ // gets one call that returns a single verdict.
15
+
16
+ import { detectRulesetDrift, getRuleset } from '#ruleset';
17
+
18
+ import { scanAcScopeCoverage, scanTcScopeVsAc } from './scope-lint.js';
19
+
20
+ /**
21
+ * @typedef {object} AdmissibilityOverride
22
+ * @property {string} rule - rule id being overridden (e.g. "NV-BL-ADM-02").
23
+ * @property {string} reason - operator-provided reason recorded on the chain.
24
+ * @property {string} [authoredBy] - author identity string.
25
+ * @property {string} [documentId] - optional pin to a specific doc id.
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} AdmissibilityVerdict
30
+ * @property {'pass' | 'refuse' | 'passWithOverrides'} verdict
31
+ * @property {import('#core/errors').RcfError[]} findings - all findings before override application.
32
+ * @property {import('#core/errors').RcfError[]} unresolved - findings not covered by a supplied override.
33
+ * @property {AdmissibilityOverride[]} appliedOverrides
34
+ * @property {object} drift - shape from detectRulesetDrift.
35
+ */
36
+
37
+ /**
38
+ * Should this finding be masked by the given override? Match on rule id
39
+ * and, when the override pins a documentId, on documentId too.
40
+ */
41
+ function overrideCovers(finding, override) {
42
+ if (finding.rule !== override.rule) return false;
43
+ if (override.documentId && finding.documentId !== override.documentId) return false;
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * Enforce admissibility across the whole chain (NV-BL-ADM-05 gate) plus
49
+ * ruleset-version drift (NV-BL-ADM-06). Callers pass the walker tree,
50
+ * the chain's declared ruleset version (per DL-REQ-VALIDATE-03), and an
51
+ * optional list of recorded overrides.
52
+ *
53
+ * A pure function on top of the ruleset + tree. Callers decide what to
54
+ * do with a `refuse` verdict (`rcf build` refuses with exit 4; a query
55
+ * or traceability tool refuses to surface the chain per the item 1
56
+ * addendum -- see `#query/refuse-on-admissibility`).
57
+ *
58
+ * @param {object} args
59
+ * @param {object} args.tree - walker output
60
+ * @param {string|null} [args.chainRulesetVersion]
61
+ * @param {AdmissibilityOverride[]} [args.overrides]
62
+ * @param {object} [args.opts]
63
+ * @param {boolean} [args.opts.tolerateUnclassified] - default true.
64
+ * @returns {Promise<AdmissibilityVerdict>}
65
+ */
66
+ export async function enforceAdmissibility({
67
+ tree,
68
+ chainRulesetVersion = null,
69
+ overrides = [],
70
+ opts = {},
71
+ } = {}) {
72
+ const ruleset = await getRuleset();
73
+ const drift = await detectRulesetDrift({ chainRulesetVersion, ruleset });
74
+
75
+ const findings = [];
76
+
77
+ // NV-BL-ADM-06: build-stage refusal on behavioural drift. Missing
78
+ // chain-ruleset-version is a separate class -- the chain never
79
+ // declared a version, so we cannot classify drift; the finding
80
+ // asks the operator to run the define-stage warning path
81
+ // (DL-REQ-VALIDATE-03) or acknowledge the omission via override.
82
+ if (drift.drift === 'behavioural') {
83
+ findings.push({
84
+ kind: 'validation',
85
+ message: `NV-BL-ADM-06: chain was authored against ruleset version ${drift.chainVersion}; shipping version is ${drift.shippingVersion}. Behaviour-changing drift refuses at build stage.`,
86
+ rule: 'NV-BL-ADM-06',
87
+ });
88
+ } else if (drift.drift === 'missing') {
89
+ findings.push({
90
+ kind: 'validation',
91
+ message: `NV-BL-ADM-06: chain does not declare a ruleset version. Run the define-stage adequacy check against ruleset ${drift.shippingVersion} (DL-REQ-VALIDATE-03) or record an override.`,
92
+ rule: 'NV-BL-ADM-06',
93
+ });
94
+ }
95
+
96
+ // NV-BL-ADM-02 / -03: scope-tag coverage + TC-scope-vs-AC-scope.
97
+ findings.push(...scanAcScopeCoverage(tree, opts));
98
+ findings.push(...scanTcScopeVsAc(tree, opts));
99
+
100
+ // NV-BL-ADM-05: refuse-first, override-recorded. Overrides are
101
+ // recorded on the chain; we apply them here to produce the
102
+ // unresolved-findings set. Source-comment markers (NV-BL-ADM-04) do
103
+ // NOT flow through this override channel; callers running that scan
104
+ // filter its findings only through the ADR-only channel per
105
+ // ruling-sheet item 16.
106
+ const appliedOverrides = [];
107
+ const unresolved = [];
108
+ for (const finding of findings) {
109
+ if (finding.rule === 'NV-BL-ADM-04') {
110
+ // Guardrail: source-marker findings must never be masked through
111
+ // the generic override channel. If a caller mistakenly threaded
112
+ // them into this function, leave them unresolved.
113
+ unresolved.push(finding);
114
+ continue;
115
+ }
116
+ const hit = overrides.find((o) => overrideCovers(finding, o));
117
+ if (hit) {
118
+ appliedOverrides.push(hit);
119
+ continue;
120
+ }
121
+ unresolved.push(finding);
122
+ }
123
+
124
+ let verdict;
125
+ if (unresolved.length === 0 && appliedOverrides.length === 0) verdict = 'pass';
126
+ else if (unresolved.length === 0) verdict = 'passWithOverrides';
127
+ else verdict = 'refuse';
128
+
129
+ return { verdict, findings, unresolved, appliedOverrides, drift };
130
+ }
131
+
132
+ /**
133
+ * Convenience: the ruleset's toolScope block. Query / traceability tools
134
+ * read this to decide whether to apply the refuse-first posture when
135
+ * surfacing chain data (NV-BL-SR-03 addendum on ruling-sheet item 1).
136
+ *
137
+ * @returns {Promise<{ chainAdmissibility: boolean, traceabilityAndQueryTools: boolean }>}
138
+ */
139
+ export async function getRulesetToolScope() {
140
+ const ruleset = await getRuleset();
141
+ return ruleset.toolScope;
142
+ }
@@ -0,0 +1,8 @@
1
+ // Public surface for the chain-admissibility gate (0.8.0 slug-train,
2
+ // car 3). Rules consumed from the shared standards ruleset bundled
3
+ // inside this umbrella package (`#ruleset`) per the ratified
4
+ // requirements doc (2026-08-06 canonical since 2026-08-12).
5
+
6
+ export { enforceAdmissibility, getRulesetToolScope } from './enforce.js';
7
+ export { scanAcScopeCoverage, scanTcScopeVsAc } from './scope-lint.js';
8
+ export { scanFilesForMarkers, scanSourceStringForMarkers } from './markers.js';
@@ -0,0 +1,104 @@
1
+ // Source-comment marker scanner (NV-BL-ADM-04).
2
+ //
3
+ // Scans a set of source files for admission markers from the ratified
4
+ // vocabulary carried on the shared standards ruleset
5
+ // (`sourceCommentMarkers[]`). Any match is a finding. Per ratified
6
+ // ruling-sheet item 16, the only permitted override channel is an ADR
7
+ // recording the deferral for a genuine external-boundary blocker; the
8
+ // generic recorded-override channel (NV-BL-ADM-05) does not cover
9
+ // source markers.
10
+ //
11
+ // This module produces findings only. Wiring the gate into
12
+ // `rcf build --mark complete` is a build-lite verb change that reads
13
+ // the FBS's build-sequence files and calls `scanSourceForMarkers` for
14
+ // each; the CLI change belongs in that verb's PR, not here.
15
+
16
+ import { readFile } from 'node:fs/promises';
17
+
18
+ import { rcfError } from '#core/errors';
19
+ import { getRuleset } from '#ruleset';
20
+
21
+ /**
22
+ * Compile the ruleset's marker vocabulary into a single case-insensitive
23
+ * pattern. Markers are ratified as case-insensitive per NV-BL-ADM-04, so
24
+ * we build the pattern with the `i` flag. Longest markers first so
25
+ * "v1 refinement" wins over "v1" if a shorter marker is ever added to
26
+ * the vocabulary in future releases.
27
+ *
28
+ * @param {Array<{ marker: string, caseInsensitive: boolean }>} markers
29
+ * @returns {RegExp}
30
+ */
31
+ function compileMarkerPattern(markers) {
32
+ const sorted = [...markers].sort((a, b) => b.marker.length - a.marker.length);
33
+ const alternation = sorted
34
+ .map((m) => m.marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
35
+ .join('|');
36
+ // Boundary-free by design: matches a marker anywhere in a comment
37
+ // ("PLACEHOLDER function" matches; "// placeholder" matches; a random
38
+ // word like "todos" also matches, which is the trade-off named in the
39
+ // ruleset's NV-BL-ADM-04 notes -- false positives on unrelated tokens
40
+ // are cheaper than false negatives on real deferrals.
41
+ return new RegExp(`(${alternation})`, 'ig');
42
+ }
43
+
44
+ /**
45
+ * Scan a single source string for the ratified marker vocabulary.
46
+ * Returns one finding per match, with line + column offsets. Empty
47
+ * input returns an empty findings array.
48
+ *
49
+ * @param {string} source
50
+ * @param {object} [opts]
51
+ * @param {string} [opts.filePath] - decorates each finding for callers
52
+ * @param {Array<{ marker: string, caseInsensitive: boolean }>} [opts.markers]
53
+ * @returns {Promise<import('#core/errors').RcfError[]>}
54
+ */
55
+ export async function scanSourceStringForMarkers(source, { filePath, markers } = {}) {
56
+ if (typeof source !== 'string' || source.length === 0) return [];
57
+ const vocab = markers ?? (await getRuleset()).sourceCommentMarkers;
58
+ const pattern = compileMarkerPattern(vocab);
59
+ const findings = [];
60
+ let match;
61
+ while ((match = pattern.exec(source)) !== null) {
62
+ const before = source.slice(0, match.index);
63
+ const line = before.split('\n').length;
64
+ const lastNewline = before.lastIndexOf('\n');
65
+ const column = match.index - (lastNewline === -1 ? -1 : lastNewline);
66
+ findings.push(rcfError({
67
+ kind: 'validation',
68
+ message: `NV-BL-ADM-04: source-comment admission marker "${match[1]}" at ${filePath ?? '<source>'}:${line}:${column}`,
69
+ filePath: filePath ?? null,
70
+ rule: 'NV-BL-ADM-04',
71
+ }));
72
+ }
73
+ return findings;
74
+ }
75
+
76
+ /**
77
+ * Scan a set of files by path for the ratified marker vocabulary. IO
78
+ * failures on a single file are captured as `ioFailure` findings; other
79
+ * files continue to scan. Order is deterministic in the input file list.
80
+ *
81
+ * @param {string[]} filePaths - absolute paths to scan
82
+ * @returns {Promise<import('#core/errors').RcfError[]>}
83
+ */
84
+ export async function scanFilesForMarkers(filePaths) {
85
+ if (!Array.isArray(filePaths) || filePaths.length === 0) return [];
86
+ const ruleset = await getRuleset();
87
+ const vocab = ruleset.sourceCommentMarkers;
88
+ const findings = [];
89
+ for (const filePath of filePaths) {
90
+ try {
91
+ const source = await readFile(filePath, 'utf8');
92
+ const perFile = await scanSourceStringForMarkers(source, { filePath, markers: vocab });
93
+ findings.push(...perFile);
94
+ } catch (err) {
95
+ findings.push(rcfError({
96
+ kind: 'ioFailure',
97
+ message: `NV-BL-ADM-04: failed to read source for marker scan: ${err.message}`,
98
+ filePath,
99
+ rule: 'NV-BL-ADM-04',
100
+ }));
101
+ }
102
+ }
103
+ return findings;
104
+ }
@@ -0,0 +1,163 @@
1
+ // Chain-admissibility scope checks (NV-BL-ADM-02, NV-BL-ADM-03).
2
+ //
3
+ // The scope-tag vocabulary itself lives on rcf-schemas 0.4.3
4
+ // (`common.$defs.scopeTag`). This module consumes the tags off ACs
5
+ // and TCs and enforces the admissibility rules the shared standards
6
+ // ruleset references (`NV-BL-ADM-02` for AC scope classification and
7
+ // `NV-BL-ADM-03` for TC scope >= AC scope).
8
+ //
9
+ // Refuse-first, override-recorded per NV-BL-ADM-05; source-comment
10
+ // markers are governed separately by NV-BL-ADM-04 and its ADR-only
11
+ // override channel.
12
+
13
+ import { rcfError } from '#core/errors';
14
+
15
+ /**
16
+ * Rank each scope so we can compare "TC scope >= AC scope" numerically.
17
+ * `unclassified` is the migration state; ranked -1 so a TC-scoped
18
+ * unclassified against a runtime-scope AC surfaces as a mismatch.
19
+ * @type {Record<string, number>}
20
+ */
21
+ const SCOPE_RANK = Object.freeze({
22
+ library: 1,
23
+ runtime: 2,
24
+ deployed: 3,
25
+ unclassified: -1,
26
+ });
27
+
28
+ /**
29
+ * True when the tag is a known scope value per the shared vocabulary.
30
+ * @param {unknown} tag
31
+ * @returns {boolean}
32
+ */
33
+ function isKnownScope(tag) {
34
+ return typeof tag === 'string' && Object.prototype.hasOwnProperty.call(SCOPE_RANK, tag);
35
+ }
36
+
37
+ /**
38
+ * NV-BL-ADM-02: every AC must carry a known scope tag. An AC without a
39
+ * scope, or with a non-vocabulary value, produces an admissibility
40
+ * finding. Per the ruleset's `unclassifiedMigrationTolerance` block,
41
+ * an AC scoped `unclassified` is currently tolerated (findings not
42
+ * emitted); callers who want to enforce full migration pass
43
+ * `tolerateUnclassified: false`.
44
+ *
45
+ * @param {object} tree - walkTree output
46
+ * @param {object} [opts]
47
+ * @param {boolean} [opts.tolerateUnclassified] - default true (migration state)
48
+ * @returns {import('#core/errors').RcfError[]}
49
+ */
50
+ export function scanAcScopeCoverage(tree, { tolerateUnclassified = true } = {}) {
51
+ const findings = [];
52
+ for (const us of tree.userStories ?? []) {
53
+ for (const ac of us.acceptanceCriteria ?? []) {
54
+ const scope = ac?.scope;
55
+ if (scope === undefined) {
56
+ findings.push(rcfError({
57
+ kind: 'validation',
58
+ message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} carries no scope tag`,
59
+ documentId: ac.id,
60
+ field: 'scope',
61
+ rule: 'NV-BL-ADM-02',
62
+ }));
63
+ continue;
64
+ }
65
+ if (!isKnownScope(scope)) {
66
+ findings.push(rcfError({
67
+ kind: 'validation',
68
+ message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} carries an unknown scope tag "${scope}"`,
69
+ documentId: ac.id,
70
+ field: 'scope',
71
+ rule: 'NV-BL-ADM-02',
72
+ }));
73
+ continue;
74
+ }
75
+ if (scope === 'unclassified' && !tolerateUnclassified) {
76
+ findings.push(rcfError({
77
+ kind: 'validation',
78
+ message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} still scoped "unclassified" after the migration window`,
79
+ documentId: ac.id,
80
+ field: 'scope',
81
+ rule: 'NV-BL-ADM-02',
82
+ }));
83
+ }
84
+ }
85
+ }
86
+ return findings;
87
+ }
88
+
89
+ /**
90
+ * NV-BL-ADM-03: for each AC, every bound TC's scope must be equal to
91
+ * or wider than the AC's scope. A library-scope TC bound to a
92
+ * runtime-scope AC surfaces as a mismatch. TCs with no scope tag are
93
+ * flagged the same way ACs are in NV-BL-ADM-02: absent = finding
94
+ * (bootstrap: unclassified tolerated).
95
+ *
96
+ * @param {object} tree - walkTree output
97
+ * @param {object} [opts]
98
+ * @param {boolean} [opts.tolerateUnclassified] - default true
99
+ * @returns {import('#core/errors').RcfError[]}
100
+ */
101
+ export function scanTcScopeVsAc(tree, { tolerateUnclassified = true } = {}) {
102
+ const findings = [];
103
+ const acScope = new Map();
104
+ for (const us of tree.userStories ?? []) {
105
+ for (const ac of us.acceptanceCriteria ?? []) {
106
+ if (ac?.id) acScope.set(ac.id, ac.scope);
107
+ }
108
+ }
109
+ for (const ts of tree.testSuites ?? []) {
110
+ for (const tc of ts.testCases ?? []) {
111
+ if (!tc?.id || !tc?.acId) continue;
112
+ const tcScope = tc?.scope;
113
+ const acTag = acScope.get(tc.acId);
114
+ if (acTag === undefined || !isKnownScope(acTag) || acTag === 'unclassified') {
115
+ // The AC's own scope problem surfaces via scanAcScopeCoverage;
116
+ // this scan is silent for ACs the classifier could not read.
117
+ continue;
118
+ }
119
+ if (tcScope === undefined) {
120
+ findings.push(rcfError({
121
+ kind: 'validation',
122
+ message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} carries no scope tag (bound AC ${tc.acId} is scope=${acTag})`,
123
+ documentId: tc.id,
124
+ field: 'scope',
125
+ rule: 'NV-BL-ADM-03',
126
+ }));
127
+ continue;
128
+ }
129
+ if (!isKnownScope(tcScope)) {
130
+ findings.push(rcfError({
131
+ kind: 'validation',
132
+ message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} carries an unknown scope tag "${tcScope}"`,
133
+ documentId: tc.id,
134
+ field: 'scope',
135
+ rule: 'NV-BL-ADM-03',
136
+ }));
137
+ continue;
138
+ }
139
+ if (tcScope === 'unclassified') {
140
+ if (!tolerateUnclassified) {
141
+ findings.push(rcfError({
142
+ kind: 'validation',
143
+ message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} still scoped "unclassified" after the migration window`,
144
+ documentId: tc.id,
145
+ field: 'scope',
146
+ rule: 'NV-BL-ADM-03',
147
+ }));
148
+ }
149
+ continue;
150
+ }
151
+ if (SCOPE_RANK[tcScope] < SCOPE_RANK[acTag]) {
152
+ findings.push(rcfError({
153
+ kind: 'validation',
154
+ message: `NV-BL-ADM-03: TC ${tc.id} scope "${tcScope}" is narrower than the AC ${tc.acId} scope "${acTag}"; a bound TC must be equal to or wider than the AC scope`,
155
+ documentId: tc.id,
156
+ field: 'scope',
157
+ rule: 'NV-BL-ADM-03',
158
+ }));
159
+ }
160
+ }
161
+ }
162
+ return findings;
163
+ }