Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions hydra-gates/scripts/lib/check_app_chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
//
// footer Documentation order 90 href to the app's docs site
// footer Store order 92 type: "store"
// footer Reports order 95 type: "reports"
// footer Features & roadmap order 100 type: "roadmap"
// settings Reports type: "reports"
// settings Flows type: "flows"
//
// The settings foldout is labelled Advanced. Reports moved there from the
// footer on 2026-10-07, on Ruben's decision (ADR-114 Decisions 1 and 3 and
// ADR-112 Decision 3, amended that day). A Reports entry in the footer or the
// main menu is now a placement finding.
//
// Measured 2026-09-03 across the 21 core apps: ONE carries all seven, and it
// is dossiq. Reports is in 3 of 19, Store in 4 of 19.
//
Expand Down Expand Up @@ -362,10 +367,11 @@ const CHROME = [
},
{
item: 'Reports',
section: 'footer',
section: 'settings',
blocking: REPORTS_IS_BLOCKING,
find: () => entries.find(({ node }) => isSurface(pageOf(node), 'reports', '/reports')),
absent: 'no Reports entry. ADR-114 Decision 3 amends ADR-112 Decision 4: the title of that ADR wins, and every app in scope declares one type:"reports" page in section:"footer" at order 95. An empty Reports page is still forbidden, so an app with nothing to report fills the page rather than skipping it.',
absent: 'no Reports entry. ADR-114 Decision 3 amends ADR-112 Decision 4: every app in scope declares one type:"reports" page and one entry for it in section:"settings", the foldout labelled Advanced (amended 2026-10-07). An empty Reports page is still forbidden, so an app with nothing to report fills the page rather than skipping it.',
misplaced: 'ADR-114 Decisions 1 and 3, amended 2026-10-07: Reports is one entry in the settings foldout, labelled Advanced, not in the footer or the main menu.',
},
{
item: 'Features & roadmap',
Expand Down Expand Up @@ -400,7 +406,8 @@ for (const spec of CHROME) {
continue
}

report(severity, spec.item, `the ${spec.item} entry is section:${JSON.stringify(hit.node.section ?? null)}, not ${JSON.stringify(spec.section)}. ADR-114 Decision 1 fixes the sections so the bottom-left of the navigation reads the same in every app.`)
const why = spec.misplaced || 'ADR-114 Decision 1 fixes the sections so the bottom-left of the navigation reads the same in every app.'
report(severity, spec.item, `the ${spec.item} entry is section:${JSON.stringify(hit.node.section ?? null)}, not ${JSON.stringify(spec.section)}. ${why}`)
}

// --- Decision 1: the footer group reads in the same ORDER everywhere --------
Expand All @@ -413,14 +420,19 @@ for (const spec of CHROME) {
// would be a fleet-wide diff that changes not one pixel.
//
// What a user can actually see is the sequence, so the sequence is the rule:
// Documentation, then Store, then Reports, then Features & roadmap. Measured
// 2026-09-03: all 19 manifest-driven apps already satisfy it, so this blocks
// nothing today and catches the next entry dropped in at the wrong number.
// Documentation, then Store, then Features & roadmap. Measured 2026-09-03:
// all 19 manifest-driven apps already satisfy it, so this blocks nothing today
// and catches the next entry dropped in at the wrong number.
//
// Reports left this sequence on 2026-10-07, when it moved to the settings
// foldout (ADR-114 Decision 1, amended). A Reports entry still in the footer is
// reported once, as a placement finding above, and is not ordered here.
{
const wanted = ['Documentation', 'Store', 'Reports', 'Features & roadmap']
const wanted = ['Documentation', 'Store', 'Features & roadmap']
const placed = []
for (const spec of CHROME) {
if (spec.section !== 'footer') { continue }
if (!wanted.includes(spec.item)) { continue }
const hit = spec.find()
if (hit === undefined || sectionOf(hit.node) !== 'footer') { continue }
const order = typeof hit.node.order === 'number' ? hit.node.order : null
Expand All @@ -433,7 +445,7 @@ for (const spec of CHROME) {
const expected = wanted.filter((w) => actual.includes(w))

if (actual.join(' > ') !== expected.join(' > ')) {
report('error', 'footer order', `the footer group reads ${JSON.stringify(actual.join(' > '))}, and ADR-114 Decision 1 orders it ${JSON.stringify(expected.join(' > '))}. The RELATIVE order is the rule, not the absolute numbers: a new entry takes Documentation 90, Store 92, Reports 95, Features & roadmap 100, but an app already running its footer at other numbers only has to keep the sequence.`)
report('error', 'footer order', `the footer group reads ${JSON.stringify(actual.join(' > '))}, and ADR-114 Decision 1 orders it ${JSON.stringify(expected.join(' > '))}. The RELATIVE order is the rule, not the absolute numbers: a new entry takes Documentation 90, Store 92, Features & roadmap 100, but an app already running its footer at other numbers only has to keep the sequence.`)
}
}

Expand Down
25 changes: 18 additions & 7 deletions hydra-gates/scripts/lib/check_reports_one_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//
// check_reports_one_page.js — gate (reports-one-page) checker.
//
// Implements ADR-112: "Reports are one page of cards, in the footer, in every
// app." Reports arrive one at a time, each one is obviously a menu item, and
// Implements ADR-112: "Reports are one page of cards, in every app." Reports
// arrive one at a time, each one is obviously a menu item, and
// the menu is where the last one went — so a Reports branch grows an entry per
// report and never loses one. shillinq reached 96 report types.
//
Expand All @@ -14,7 +14,10 @@
//
// 1. AT MOST ONE `type: "reports"` page. Two reports pages is the submenu
// problem again, one level down.
// 2. The menu entry pointing at it is `section: "footer"` (ADR-112 D3).
// 2. The menu entry pointing at it is `section: "settings"`, the foldout
// the shell labels Advanced (ADR-112 D3, amended 2026-10-07 on Ruben's
// decision). It used to be `section: "footer"`; a Reports entry in the
// footer or in the main menu is now a finding.
// 3. No OTHER menu entry points at a page that the reports page already
// lists as a card. That is the duplication the ADR exists to remove: the
// report is reachable twice, and the menu copy is the one that never
Expand Down Expand Up @@ -155,21 +158,29 @@ if (reportsPages.length > 1) {

const reportsPage = reportsPages[0]

// 2. Its menu entry sits in the footer group.
// 2. Its menu entry sits in the settings foldout, labelled Advanced.
//
// ADR-112 Decision 3 as amended 2026-10-07 (Ruben's decision): the shell's
// settings foldout is renamed Advanced, and Reports is one entry inside it.
// Before that date the rule was `section: "footer"`, and the footer is now
// exactly as wrong as the main menu, so the message names both.
const REPORTS_SECTION = 'settings'
const reportsEntry = entries.find(({ node }) => node.route === reportsPage.id)
if (reportsEntry === undefined) {
findings.push({
severity: 'error',
path: rel,
pageIds: [reportsPage.id],
message: `the type:"reports" page "${reportsPage.id}" has no menu entry, so nothing reaches it. ADR-112 Decision 3 puts one in the footer group.`,
message: `the type:"reports" page "${reportsPage.id}" has no menu entry, so nothing reaches it. ADR-112 Decision 3 (amended 2026-10-07) puts one entry in the settings foldout, labelled Advanced.`,
})
} else if (reportsEntry.node.section !== 'footer') {
} else if (reportsEntry.node.section !== REPORTS_SECTION) {
const sec = reportsEntry.node.section
const where = sec === 'footer' ? 'the footer' : ((sec === undefined || sec === null || sec === '' || sec === 'main') ? 'the main menu' : `the "${sec}" section`)
findings.push({
severity: 'error',
path: rel,
pageIds: [reportsPage.id],
message: `the Reports menu entry is section:${JSON.stringify(reportsEntry.node.section ?? null)}, not "footer". ADR-112 Decision 3: Reports is a place you go deliberately, not a place you work, so it belongs with Documentation and Features & roadmap rather than among the operational entries.`,
message: `the Reports menu entry is section:${JSON.stringify(reportsEntry.node.section ?? null)}, not "settings", so it renders in ${where}. ADR-112 Decision 3, amended 2026-10-07: Reports is one entry in the settings foldout, labelled Advanced, not in the footer or the main menu. Reports is a place you go deliberately, not a place you work.`,
})
}

Expand Down
12 changes: 7 additions & 5 deletions hydra-gates/scripts/run-hydra-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11246,14 +11246,15 @@ if [ -f src/manifest.json ]; then
fi

# ---------------------------------------------------------------------------
# Gate 104: reports-one-page — reports are cards on one page, in the footer.
# Gate 104: reports-one-page — reports are cards on one page, in the settings
# foldout labelled Advanced (ADR-112 D3, amended 2026-10-07; was the footer).
#
# ADR-112. Reports arrive one at a time, each looks like a menu item, and the
# menu is where the last one went, so a Reports branch grows an entry per
# report and never loses one (shillinq reached 96 report types).
#
# Checks three STRUCTURAL things against the effective manifest: at most one
# type:"reports" page; its menu entry is section:"footer"; and no other menu
# type:"reports" page; its menu entry is section:"settings"; and no other menu
# entry points at a page that page already carries as a card.
#
# NOT APPLICABLE when the app declares no type:"reports" page — ADR-112
Expand Down Expand Up @@ -11328,7 +11329,7 @@ if [ -f src/manifest.json ]; then
if [ "${_rop_rc}" -eq 0 ]; then
_pass 104 "reports-one-page"
else
_fail 104 "reports-one-page" "${_rop_reported} finding(s) against ADR-112 (one reports page, in the footer, no report reachable both as a card and as a menu entry) — see ${_rop_log}"
_fail 104 "reports-one-page" "${_rop_reported} finding(s) against ADR-112 (one reports page, its entry in the settings foldout labelled Advanced, no report reachable both as a card and as a menu entry) — see ${_rop_log}"
fi
;;
esac
Expand All @@ -11341,8 +11342,9 @@ fi
#
# ADR-114. CnAppNav draws Personal settings and Admin settings itself and no
# app declares them (ADR-079 / ADR-110 D2). The other five are the app's own:
# Documentation, Store, Reports and Features & roadmap in `footer`, Flows in
# the settings foldout. Measured 2026-09-03: ONE of 21 apps carries all seven.
# Documentation, Store and Features & roadmap in `footer`, Reports and Flows in
# the settings foldout labelled Advanced (Reports moved there from the footer
# on 2026-10-07, ADR-114 D1 and D3 as amended). Measured 2026-09-03: ONE of 21 apps carries all seven.
#
# WHY THIS IS NOT COVERED BY THE FOUR GATES THAT ALREADY READ THIS MENU:
# every one of them is presence-blind. gate-60 validates the icon on an entry
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<id>acfx</id>
<name>App Chrome Fixture</name>
<summary>Fixture app for gate-107 acceptance. Not a real app.</summary>
<description>Exists so the gate suite can drive an app whose bottom-left chrome is and is not the seven items ADR-114 requires, through the real effective-manifest builder.</description>
<version>1.0.0</version>
<licence>agpl</licence>
<author>Conduction</author>
<namespace>AcFx</namespace>
<category>tools</category>
<dependencies><nextcloud min-version="30" max-version="32"/></dependencies>
</info>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
{
"$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
"version": "2.0.0",
"nav": {
"includePersonalSettings": false,
"_note": "ANTI-WIDENING 4: suppressing the auto-prepended entry is LEGITIMATE when the app declares its own action:\"user-settings\" entry, which opens the same dialog. keepiq does exactly this and injects its own sections into CnAppRoot's #user-settings slot; re-enabling the shell copy would give it two entries onto one dialog. A gate that flagged the flag alone would red keepiq."
},
"_note": "CLEAN arm: the same app with ReportsMenu in section:\"settings\", the foldout labelled Advanced (amended 2026-10-07). Everything else matches the planted arm.",
"menu": [
{
"id": "DashboardMenu",
"label": "Dashboard",
"icon": "ViewDashboardOutline",
"route": "Dashboard",
"order": 10
},
{
"id": "FlowRunsMenu",
"label": "Flow runs",
"icon": "PlayCircleOutline",
"route": "FlowRuns",
"order": 20,
"_note": "ANTI-WIDENING 1: an operational list whose LABEL contains 'flow' but which is an ordinary index, in main, and is not the flow authoring surface. A gate that matched Flows on the label rather than on the page type would accept this as the Flows entry in the planted arm and would flag its placement here. Detection is by page type for exactly this reason."
},
{
"id": "DocsMenu",
"label": "Documentation",
"icon": "BookOpenVariantOutline",
"href": "https://example.org",
"order": 90,
"section": "footer"
},
{
"id": "StoreMenu",
"label": "Store",
"icon": "StoreOutline",
"route": "Store",
"order": 92,
"section": "footer"
},
{
"id": "ReportsMenu",
"label": "Reports",
"icon": "ChartBoxOutline",
"route": "Reports",
"order": 95,
"section": "settings"
},
{
"id": "RoadmapMenu",
"label": "Features & roadmap",
"icon": "MapMarkerPath",
"route": "FeaturesRoadmap",
"order": 100,
"section": "footer"
},
{
"id": "UserSettings",
"label": "Personal settings",
"icon": "CogOutline",
"action": "user-settings",
"section": "settings",
"order": 1
},
{
"id": "ConfigGroup",
"label": "Configuration",
"icon": "CogOutline",
"section": "settings",
"order": 50,
"children": [
{
"id": "FlowsMenu",
"label": "Flows",
"icon": "Sitemap",
"route": "Flows",
"order": 10,
"section": "settings",
"_note": "ANTI-WIDENING 2: nested under a group rather than declared top-level. ADR-110's own fleet audit walked only top-level menu[] entries, read integriq's nested Flows as absent, and had to withdraw the claim inside the ADR. A gate that does not descend into children[] turns this clean arm red."
}
]
}
],
"pages": [
{
"id": "Dashboard",
"route": "/",
"type": "dashboard",
"title": "Dashboard",
"config": {
"widgets": []
}
},
{
"id": "FlowRuns",
"route": "/flow-runs",
"type": "index",
"title": "Flow runs",
"config": {
"register": "acfx",
"schema": "case",
"columns": [
"name"
]
}
},
{
"id": "Flows",
"route": "/flows",
"type": "index",
"title": "Flows",
"config": {
"entitySource": "flows",
"app": "acfx",
"rowRoute": "FlowDetail"
},
"_note": "The CANONICAL flow index: type:\"index\" over the named `flows` entity source. nextcloud-vue marks the dedicated `flows` / `flow-detail` page types DEPRECATED in CnPageRenderer/pageTypes.js, kept only so the fleet can migrate without a flag day. The gate must recognise this shape, because it is the one new apps are supposed to write."
},
{
"id": "FlowDetail",
"route": "/flows/:id",
"type": "flow",
"title": "Flow",
"config": {
"app": "acfx"
}
},
{
"id": "Store",
"route": "/store",
"type": "custom",
"component": "StoreGallery",
"title": "Store",
"config": {
"storeRegistry": true
},
"_note": "The shape dossiq ships today. `store` is NOT yet a page type in the manifest schema, so a fixture declaring one would red gate-22 while proving nothing about gate-107. When nextcloud-vue absorbs dossiq's StoreGallery as CnStorePage and the enum gains `store`, this becomes type:\"store\" and the checker already accepts it."
},
{
"id": "Reports",
"route": "/reports",
"type": "reports",
"title": "Reports",
"config": {
"cards": [
{
"id": "t",
"label": "Throughput",
"route": "Throughput"
}
]
}
},
{
"id": "Throughput",
"route": "/reports/throughput",
"type": "index",
"title": "Throughput",
"config": {
"register": "acfx",
"schema": "case",
"columns": [
"name"
]
}
},
{
"id": "FeaturesRoadmap",
"route": "/features-roadmap",
"type": "custom",
"component": "FeaturesRoadmapIndex",
"title": "Features & roadmap",
"_note": "ANTI-WIDENING 3: the roadmap surface is type:\"custom\" at the canonical path, which is what openregister actually ships (\"the built-in roadmap page is not adopted in this shell-swap change\"), and what shillinq ships for reports. A gate that insisted on the page TYPE would call this MISSING and red an app that is right. Reverting isSurface() to a type-only test turns this clean arm red, which is the point: the regression was found on the live fleet before the gate merged, and this is what stops it coming back."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# expect.conf — read by scripts/lib/test_gate_acceptance_matrix.sh
#
# Columns:
# gate <n> <log-basename|-> <planted-verdict> <clean-verdict> <subject-substring>
#
# Bundle: app-chrome-reports-footer — gate-107 (ADR-114 Decisions 1 and 3,
# amended 2026-10-07).
#
# Ruben decided on 2026-10-07 that the settings foldout is labelled Advanced
# and that Reports lives in it, in every app. Until then ADR-114 put Reports in
# the footer at order 95, so every app that followed the old rule now has
# exactly this planted shape.
#
# Both arms are copies of the app-chrome CLEAN arm, so every other chrome item
# is present and correctly placed, and the anti-widening controls of that
# bundle ride along. The arms differ in ONE field: ReportsMenu's section. The
# planted arm has it in `footer` and must fail naming the new rule; the clean
# arm has it in `settings` and must pass.
gate 107 hydra-gate-app-chrome.log FAIL PASS Reports is one entry in the settings foldout, labelled Advanced
Loading
Loading