Compare commits
No commits in common. "render" and "main" have entirely different histories.
14
.forgejo/workflows/issue.yml
Normal file
14
.forgejo/workflows/issue.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
on:
|
||||
issues:
|
||||
types: [opened, reopened, closed, labeled, edited]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: self-hosted-nixos-x86_64
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- name: Check techtree-manager presence
|
||||
run: |
|
||||
test -e ~/.cache/fafo-techtree/techtree-manager
|
||||
- name: Run techtree-manager
|
||||
run: cd techtree-manager/ && nix-shell shell.nix --run ~/.cache/fafo-techtree/techtree-manager
|
17
.forgejo/workflows/push.yml
Normal file
17
.forgejo/workflows/push.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'poc'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted-nixos-x86_64
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- name: Build techtree manager tool
|
||||
run: cd techtree-manager/ && nix-shell shell.nix --run "cargo build --release"
|
||||
- name: Cache the techtree manager tool
|
||||
run: |
|
||||
mkdir -p ~/.cache/fafo-techtree
|
||||
cp -v techtree-manager/target/release/techtree-manager ~/.cache/fafo-techtree/techtree-manager
|
140
README.md
Normal file
140
README.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
FAFO Technology Tree
|
||||
====================
|
||||
This repository tracks our "technology tree" — the steps towards our
|
||||
overarching goals.
|
||||
|
||||
A description of how this tech tree works can be found below. See
|
||||
[Working with the Tech Tree](#working-with-the-tech-tree).
|
||||
|
||||
## Full Technology Tree
|
||||
Below, you can see the full technology tree. The issues in this repository
|
||||
track the tree's elements. For a better overview, check each issue for a
|
||||
filtered subtree that contains just the elements related to it.
|
||||
|
||||

|
||||
|
||||
## Working with the Tech Tree
|
||||
Fundamentally, the tech tree is built from the issues in this repository and
|
||||
their interdependencies. For elements to be reached, create new issues and
|
||||
select one of the `Type/###` labels to declare what kind it is:
|
||||
|
||||
- Type/**Equipment** 🔬 — A piece of machinery that we need to acquire or get
|
||||
running. Mainly things that can be bought instead of built.
|
||||
|
||||
- Type/**Process** ⚗️ — A process we need to achieve. This means we need to
|
||||
become able to perform this process reliably.
|
||||
|
||||
- Type/**Development** 🔩 — A device, software, or other thing we need to develop.
|
||||
This is _engineering_ work; making use of existing knowledge to build
|
||||
something useful.
|
||||
|
||||
- Type/**Research** 🧪 — A topic that we can or must research to unlock future
|
||||
capabilities. In contrast to _Development_ elements, we cannot make use of
|
||||
prior art here. So less engineering and more _science_.
|
||||
|
||||
#### Dependencies
|
||||
Once created, add dependencies between issues to model their relationships.
|
||||
The CI of this repository will automatically update the tech tree accordingly.
|
||||
In addition, each issue gets a partial representation of the subtree of
|
||||
elements directly related to it. You can quickly see what is still missing to
|
||||
achieve a particular element. And also what next steps will be unlocked once
|
||||
an element has been achieved.
|
||||
|
||||
There are no distinct types of dependencies. This was a choice in the name of
|
||||
keeping the model simple. All dependencies are hard requirements. But also
|
||||
check the next section on more thoughts about this...
|
||||
|
||||
#### Modelling Approach
|
||||
Finding the right balance between model complexity and expressiveness is
|
||||
tricky. Following are some guidelines for adding elements to this tech tree.
|
||||
|
||||
All elements should have a few fundamental properties, to keep the model consistent:
|
||||
|
||||
- Elements shall have a **clear and unambiguous acceptance criterion**. If
|
||||
necessary, this can be elaborated on in the issue body. "SEM Imaging" leaves
|
||||
open what scale we can image reliably. It may be sensible to add multiple
|
||||
elements to model progress in such a domain.
|
||||
|
||||
- Elements shall be **actionable**, in the sense that someone can put in effort to
|
||||
achieve them. Having a good acceptance criterion does most of the heavy
|
||||
lifting here.
|
||||
|
||||
- Elements shall be **necessary** for our bigger visions. Ultimate elements
|
||||
(elements that nothing depends on) should get special consideration in this
|
||||
regard. If you have visions of your own, it is of course fine to make an
|
||||
ultimate element for them.
|
||||
|
||||
- Elements shall have a scope/granularity size that is appropriate for tracking
|
||||
progress. We will need to figure this out as we go.
|
||||
|
||||
Generally, our tech tree is a living object. It should be updated as we figure
|
||||
out more elements to be tracked and their dependencies.
|
||||
|
||||
One topic of particular interest are "path choices". We can either use
|
||||
technology A or technology B to achieve element C:
|
||||
|
||||
```mermaid
|
||||
flowchart BT
|
||||
classDef eoi fill:#fff, stroke:#000, color:#000;
|
||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
||||
classDef dep_missing fill:#fcc, stroke:#800, color:#000;
|
||||
classDef dep_assigned fill:#ffa, stroke:#a50, color:#000;
|
||||
classDef dep_completed fill:#afa, stroke:#080, color:#000;
|
||||
0:::dep_missing
|
||||
0["#1 | MISSING\n<i>Process</i>\n<b>Technology A</b>"]
|
||||
1:::dep_completed
|
||||
1["#2 | COMPLETED\n<i>Process</i>\n<b>Technology B</b>"]
|
||||
2:::eoi
|
||||
2["#3 | MISSING\n<i>Process</i>\n<b>Element C</b>"]
|
||||
2 --> 0
|
||||
2 --> 1
|
||||
```
|
||||
|
||||
Dependencies cannot express such choices. Instead, please follow this approach:
|
||||
|
||||
- Initially, all potential path choice dependencies are added. The issue body
|
||||
shall document the choice options and implications.
|
||||
|
||||
- When we get closer to the element of interest, we make the choice of what
|
||||
path to pursue. At this time, the other dependencies are dropped. The
|
||||
tech-tree now only documents the path choice we have made.
|
||||
|
||||
Another situation we will encounter is the need to use generic equipment for
|
||||
specific purposes. If the specific purpose is non-trivial, it shall be
|
||||
modelled as an intermediate _Process_ element in-between:
|
||||
|
||||
```mermaid
|
||||
flowchart BT
|
||||
classDef eoi fill:#fff, stroke:#000, color:#000;
|
||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
||||
classDef dep_missing fill:#fcc, stroke:#800, color:#000;
|
||||
classDef dep_assigned fill:#ffa, stroke:#a50, color:#000;
|
||||
classDef dep_completed fill:#afa, stroke:#080, color:#000;
|
||||
0:::dep_missing
|
||||
0["#2 | MISSING\n<i>Process</i>\n<b>Processing X using machine A</b>"]
|
||||
1:::dep_completed
|
||||
1["#1 | COMPLETED\n<i>Equipment</i>\n<b>Generic Machine A</b>"]
|
||||
2:::eoi
|
||||
2["#3 | MISSING\n<i>Process</i>\n<b>Element C</b>"]
|
||||
2 --> 0
|
||||
0 --> 1
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Element Status
|
||||
The status of each element is determined as follows:
|
||||
|
||||
- **MISSING** when the element has not yet been achieved.
|
||||
- **ASSIGNED** when the issue has been assigned to someone. This means we are making progress!
|
||||
- **COMPLETED** when the issue is labelled `Completed`. Achievement unlocked!
|
||||
|
||||
#### Important notes
|
||||
There are a few gotchas that you should be aware of:
|
||||
|
||||
- The CI will not automatically update when changing dependencies,
|
||||
unfortunately. You can force a trigger by editing the issue body or quickly
|
||||
adding and then removing the `Stale` label.
|
||||
|
||||
- Issues that should no longer be a part of the techtree should either be
|
||||
deleted (looses all tracking) or they can be made inert by closing them.
|
2
techtree-manager/.gitignore
vendored
Normal file
2
techtree-manager/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target/
|
||||
/render-git/
|
1987
techtree-manager/Cargo.lock
generated
Normal file
1987
techtree-manager/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
22
techtree-manager/Cargo.toml
Normal file
22
techtree-manager/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "techtree-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["rahix <rahix@rahix.de>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.41"
|
||||
env_logger = { version = "0.11.8", default-features = false, features = ["auto-color", "color", "humantime"] }
|
||||
forgejo-api = { git = "https://git.fa-fo.de/rahix/forgejo-api.git", rev = "a3f6452cfe774898a89ac66be393e5205f5e12b7" }
|
||||
log = "0.4.27"
|
||||
petgraph = "0.8.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
sha256 = "1.6.0"
|
||||
time = "0.3.41"
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
url = "2.5.4"
|
201
techtree-manager/LICENSE-APACHE
Normal file
201
techtree-manager/LICENSE-APACHE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
23
techtree-manager/LICENSE-MIT
Normal file
23
techtree-manager/LICENSE-MIT
Normal file
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
21
techtree-manager/shell.nix
Normal file
21
techtree-manager/shell.nix
Normal file
|
@ -0,0 +1,21 @@
|
|||
# From 2025-05-21
|
||||
let pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/36ecfe6216f0aa7f2a1ffe5aafc2c0eae6c8cdcf.tar.gz")) {};
|
||||
|
||||
in pkgs.mkShell {
|
||||
buildInputs = [
|
||||
# Rust
|
||||
pkgs.cargo
|
||||
pkgs.rustc
|
||||
pkgs.rustfmt
|
||||
|
||||
# Dependencies
|
||||
pkgs.openssl
|
||||
pkgs.graphviz
|
||||
pkgs.git
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export OPENSSL_DIR="${pkgs.openssl.dev}"
|
||||
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
|
||||
'';
|
||||
}
|
145
techtree-manager/src/collect.rs
Normal file
145
techtree-manager/src/collect.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
/// Read all issues to generate the full techtree
|
||||
pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result<crate::tree::Tree> {
|
||||
let mut issues = vec![];
|
||||
for page in 1.. {
|
||||
let new = ctx
|
||||
.forgejo
|
||||
.issue_list_issues(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
forgejo_api::structs::IssueListIssuesQuery {
|
||||
// We also want the closed issues
|
||||
state: Some(forgejo_api::structs::IssueListIssuesQueryState::All),
|
||||
// We cannot turn off pagination entirely, but let's set the limit as high as
|
||||
// Forgejo lets us.
|
||||
limit: Some(10000),
|
||||
page: Some(page),
|
||||
// Only issues
|
||||
r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed fetching page {page} of the issue list"))?;
|
||||
|
||||
if new.len() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
issues.extend(new);
|
||||
}
|
||||
|
||||
let mut tree = crate::tree::Tree::new();
|
||||
let mut issue_numbers = Vec::new();
|
||||
|
||||
for issue in issues.iter() {
|
||||
let element = match element_from_issue(issue) {
|
||||
Ok(el) => el,
|
||||
Err(e) => {
|
||||
let maybe_number = issue
|
||||
.number
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or("<unknown>".to_owned());
|
||||
log::warn!("Failed processing issue #{maybe_number}: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
issue_numbers.push(element.issue_number);
|
||||
tree.add_element(element);
|
||||
}
|
||||
|
||||
for issue in issue_numbers.into_iter() {
|
||||
let dependencies = ctx
|
||||
.forgejo
|
||||
.issue_list_issue_dependencies(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
// Why the hell is the issue number a string here?
|
||||
&issue.to_string(),
|
||||
forgejo_api::structs::IssueListIssueDependenciesQuery {
|
||||
limit: Some(10000),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to fetch issue dependencies for #{issue}",))?;
|
||||
|
||||
for dep in dependencies {
|
||||
let dep_number = dep.number.context("Missing issue number in dependency")?;
|
||||
if !tree.find_element_by_issue_number(dep_number).is_some() {
|
||||
log::warn!("Found dependency from #{issue} on non-tracked issue #{dep_number}!");
|
||||
} else {
|
||||
tree.add_dependency_by_issue_number(issue, dep_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result<crate::tree::Element> {
|
||||
let issue_number = issue.number.context("Missing issue number")?;
|
||||
let description = issue
|
||||
.title
|
||||
.as_deref()
|
||||
.context("Issue is missing a title")?
|
||||
.to_owned();
|
||||
|
||||
let labels = issue
|
||||
.labels
|
||||
.as_ref()
|
||||
.context("Issue does not have any labels")?;
|
||||
|
||||
let ty_labels: Vec<_> = labels
|
||||
.iter()
|
||||
.filter_map(|l| l.name.as_deref())
|
||||
.filter(|l| l.starts_with("Type/"))
|
||||
.collect();
|
||||
|
||||
if ty_labels.len() == 0 {
|
||||
anyhow::bail!("Issue #{issue_number} has no type label!");
|
||||
}
|
||||
if ty_labels.len() > 1 {
|
||||
anyhow::bail!("Issue #{issue_number} has more than one type label!");
|
||||
}
|
||||
|
||||
let ty = ty_labels
|
||||
.first()
|
||||
.unwrap()
|
||||
.strip_prefix("Type/")
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let has_completed_label = labels
|
||||
.iter()
|
||||
.any(|l| l.name.as_deref() == Some("Completed"));
|
||||
|
||||
let status = match issue.state.context("Missing issue state")? {
|
||||
forgejo_api::structs::StateType::Open => {
|
||||
if has_completed_label {
|
||||
crate::tree::ElementStatus::Completed
|
||||
} else if issue.assignee.is_some()
|
||||
|| issue
|
||||
.assignees
|
||||
.as_ref()
|
||||
.map(|v| v.len() > 0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
crate::tree::ElementStatus::Assigned
|
||||
} else {
|
||||
crate::tree::ElementStatus::Missing
|
||||
}
|
||||
}
|
||||
forgejo_api::structs::StateType::Closed => anyhow::bail!("Ignoring closed issue!"),
|
||||
};
|
||||
|
||||
Ok(crate::tree::Element {
|
||||
issue_number,
|
||||
description,
|
||||
ty,
|
||||
status,
|
||||
})
|
||||
}
|
57
techtree-manager/src/event_meta.rs
Normal file
57
techtree-manager/src/event_meta.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IssueEventMeta {
|
||||
pub action: IssueAction,
|
||||
pub issue: IssueMeta,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum IssueAction {
|
||||
Opened,
|
||||
Reopened,
|
||||
Closed,
|
||||
Assigned,
|
||||
Unassigned,
|
||||
Edited,
|
||||
#[serde(rename = "label_updated")]
|
||||
LabelUpdated,
|
||||
Labeled,
|
||||
#[serde(rename = "label_cleared")]
|
||||
LabelCleared,
|
||||
Unlabeled,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IssueMeta {
|
||||
pub number: u64,
|
||||
pub repository: RepoMeta,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepoMeta {
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
pub fn get_issue_event_meta_from_env() -> anyhow::Result<IssueEventMeta> {
|
||||
let path = std::env::var_os("GITHUB_EVENT_PATH")
|
||||
.context("Could not get event description file path (GITHUB_EVENT_PATH)")?;
|
||||
let f = std::fs::File::open(path).context("Could not open GITHUB_EVENT_PATH file")?;
|
||||
let meta: IssueEventMeta = serde_json::de::from_reader(f).context("Failed to parse")?;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
pub fn fake() -> IssueEventMeta {
|
||||
IssueEventMeta {
|
||||
action: IssueAction::Edited,
|
||||
issue: IssueMeta {
|
||||
number: 1337,
|
||||
repository: RepoMeta {
|
||||
name: "techtree".to_owned(),
|
||||
owner: "fafo".to_owned(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
186
techtree-manager/src/issue.rs
Normal file
186
techtree-manager/src/issue.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
pub type CommentId = u64;
|
||||
|
||||
pub struct BotCommentInfo {
|
||||
pub body: String,
|
||||
pub id: CommentId,
|
||||
}
|
||||
|
||||
pub async fn make_bot_comment(
|
||||
ctx: &crate::Context,
|
||||
issue_number: u64,
|
||||
) -> anyhow::Result<BotCommentInfo> {
|
||||
let initial_message =
|
||||
"_Please be patient, this issue is currently being integrated into the techtree..._";
|
||||
|
||||
let res = ctx
|
||||
.forgejo
|
||||
.issue_create_comment(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
issue_number,
|
||||
forgejo_api::structs::CreateIssueCommentOption {
|
||||
body: initial_message.to_owned(),
|
||||
updated_at: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(BotCommentInfo {
|
||||
id: res.id.unwrap(),
|
||||
body: initial_message.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn find_bot_comment(
|
||||
ctx: &crate::Context,
|
||||
issue_number: u64,
|
||||
) -> anyhow::Result<Option<BotCommentInfo>> {
|
||||
let mut comments = ctx
|
||||
.forgejo
|
||||
.issue_get_comments(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
issue_number,
|
||||
forgejo_api::structs::IssueGetCommentsQuery {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed fetching comments for issue")?;
|
||||
|
||||
comments.sort_by_key(|comment| comment.created_at);
|
||||
|
||||
let maybe_bot_comment = comments
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|comment| comment.user.as_ref().unwrap().id == Some(-2));
|
||||
|
||||
Ok(maybe_bot_comment.map(|c| BotCommentInfo {
|
||||
body: c.body.clone().unwrap_or("".to_owned()),
|
||||
id: c.id.unwrap(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Find existing bot comment or create a new one.
|
||||
///
|
||||
/// Returns a tuple of the comment information and a boolean indicating whether the comment was
|
||||
/// newly created.
|
||||
pub async fn find_or_make_bot_comment(
|
||||
ctx: &crate::Context,
|
||||
issue_number: u64,
|
||||
) -> anyhow::Result<(BotCommentInfo, bool)> {
|
||||
if let Some(comment) = find_bot_comment(ctx, issue_number)
|
||||
.await
|
||||
.context("Failed to search for bot comment in issue")?
|
||||
{
|
||||
Ok((comment, false))
|
||||
} else {
|
||||
make_bot_comment(ctx, issue_number)
|
||||
.await
|
||||
.context("Failed to make new bot comment in issue")
|
||||
.map(|c| (c, true))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_bot_comment(
|
||||
ctx: &crate::Context,
|
||||
id: CommentId,
|
||||
new_body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
ctx.forgejo
|
||||
.issue_edit_comment(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
id,
|
||||
forgejo_api::structs::EditIssueCommentOption {
|
||||
body: new_body,
|
||||
updated_at: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to update comment body")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_bot_comment_from_subtree(
|
||||
ctx: &crate::Context,
|
||||
id: CommentId,
|
||||
subtree: &crate::tree::Subtree<'_>,
|
||||
hash: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), false);
|
||||
|
||||
// When the mermaid graph gets too big, regenerate a simplified version.
|
||||
if mermaid.len() > 4990 {
|
||||
log::info!("Mermaid graph is too big, generating simplified version...");
|
||||
mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), true);
|
||||
}
|
||||
|
||||
let full_text = if mermaid.len() > 4990 {
|
||||
format!(
|
||||
r##"## Partial Techtree
|
||||
_Sorry, the partial techtree is still too big to be displayed for this element..._
|
||||
|
||||
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
|
||||
timestamp = ctx.timestamp,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r##"## Partial Techtree
|
||||
```mermaid
|
||||
{mermaid}
|
||||
```
|
||||
|
||||
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
|
||||
timestamp = ctx.timestamp,
|
||||
)
|
||||
};
|
||||
|
||||
update_bot_comment(&ctx, id, full_text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_stale_label(ctx: &crate::Context, issue_number: u64) -> anyhow::Result<()> {
|
||||
let labels = ctx
|
||||
.forgejo
|
||||
.issue_get_labels(&ctx.owner, &ctx.repo, issue_number)
|
||||
.await
|
||||
.context("Failed fetching issue labels")?;
|
||||
|
||||
let stale_label_id = labels
|
||||
.iter()
|
||||
.filter(|l| l.name.as_deref() == Some("Stale"))
|
||||
.next()
|
||||
.map(|l| l.id.unwrap());
|
||||
|
||||
if let Some(stale_label_id) = stale_label_id {
|
||||
log::info!("Removing `Stale` label from issue #{issue_number}...");
|
||||
|
||||
let res = ctx
|
||||
.forgejo
|
||||
.issue_remove_label(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
issue_number,
|
||||
stale_label_id,
|
||||
forgejo_api::structs::DeleteLabelsOption {
|
||||
updated_at: Some(time::OffsetDateTime::now_utc()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
// At the moment, the token for Forgejo Actions cannot remove issue labels.
|
||||
// See https://codeberg.org/forgejo/forgejo/issues/2415
|
||||
log::warn!(
|
||||
"Failed to remove `Stale` label for #{issue_number}. This is a known Forgejo limitation at the moment... ({e})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
152
techtree-manager/src/main.rs
Normal file
152
techtree-manager/src/main.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::Context as _;
|
||||
use forgejo_api::Forgejo;
|
||||
|
||||
mod collect;
|
||||
mod event_meta;
|
||||
mod issue;
|
||||
mod render;
|
||||
mod tree;
|
||||
mod wiki;
|
||||
|
||||
pub struct Context {
|
||||
/// API Accessor object
|
||||
pub forgejo: forgejo_api::Forgejo,
|
||||
/// Repository Owner
|
||||
pub owner: String,
|
||||
/// Repository Name
|
||||
pub repo: String,
|
||||
/// URL of the repository page
|
||||
pub repo_url: url::Url,
|
||||
/// URL of the repository with authentication information attached
|
||||
pub repo_auth_url: url::Url,
|
||||
/// Human readable timestamp of this manager run
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
async fn run() -> anyhow::Result<()> {
|
||||
let meta = if std::env::var("TECHTREE_FAKE").ok().is_some() {
|
||||
log::warn!("Fake tree!");
|
||||
event_meta::fake()
|
||||
} else {
|
||||
event_meta::get_issue_event_meta_from_env().context("Failed reading issue event data")?
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"Running due to event \"{:?}\" on issue #{} ...",
|
||||
meta.action,
|
||||
meta.issue.number
|
||||
);
|
||||
|
||||
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
log::info!("Timestamp of this run is {timestamp}");
|
||||
|
||||
let token =
|
||||
std::env::var("GITHUB_TOKEN").context("Failed accessing GITHUB_TOKEN auth token")?;
|
||||
|
||||
let server_url = url::Url::parse(
|
||||
&std::env::var("GITHUB_SERVER_URL")
|
||||
.context("Failed reading GITHUB_SERVER_URL server url")?,
|
||||
)
|
||||
.context("Failed parsing GITHUB_SERVER_URL as a url")?;
|
||||
|
||||
let repo_url = server_url
|
||||
.join(&format!(
|
||||
"{}/{}",
|
||||
meta.issue.repository.owner, meta.issue.repository.name
|
||||
))
|
||||
.unwrap();
|
||||
let mut repo_auth_url = repo_url.clone();
|
||||
repo_auth_url.set_username("forgejo-actions").unwrap();
|
||||
repo_auth_url.set_password(Some(&token)).unwrap();
|
||||
|
||||
let auth = forgejo_api::Auth::Token(&token);
|
||||
let forgejo =
|
||||
Forgejo::new(auth, server_url).context("Could not create Forgejo API access object")?;
|
||||
|
||||
let ctx = Context {
|
||||
forgejo,
|
||||
owner: meta.issue.repository.owner.clone(),
|
||||
repo: meta.issue.repository.name.clone(),
|
||||
repo_url,
|
||||
repo_auth_url,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
if meta.action == event_meta::IssueAction::Opened {
|
||||
log::debug!("Running for a newly opened issue. Taking care of the comment first...");
|
||||
match issue::find_or_make_bot_comment(&ctx, meta.issue.number).await {
|
||||
Ok((_comment, is_new)) => {
|
||||
if is_new {
|
||||
log::info!(
|
||||
"Waiting for a minute, as the issue is brand new. We will likely catch some more updates this way!"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Error while commenting on new issue #{}, continuing anyway... Error: {err:?}",
|
||||
meta.issue.number
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Collecting tree from issue metadata...");
|
||||
let tree = collect::collect_tree(&ctx)
|
||||
.await
|
||||
.context("Failed to collect the techtree from issue metadata")?;
|
||||
|
||||
log::info!("Rendering and publishing techtree to git repository...");
|
||||
let rendered_tree = render::render(&ctx, &tree)
|
||||
.await
|
||||
.context("Failed to render the techtree")?;
|
||||
render::publish(&ctx, &rendered_tree)
|
||||
.await
|
||||
.context("Failed to publish rendered tree to git")?;
|
||||
|
||||
// Wiki is disabled because the tree is too big for mermaid to handle
|
||||
if false {
|
||||
log::info!("Updating the wiki overview...");
|
||||
wiki::update_wiki_from_tree(&ctx, &tree)
|
||||
.await
|
||||
.context("Failed to update the techtree wiki page")?;
|
||||
}
|
||||
|
||||
'issues: for issue in tree.iter_issues() {
|
||||
let subtree = tree.subtree_for_issue(issue).unwrap();
|
||||
let hash = subtree.stable_hash();
|
||||
|
||||
let (bot_comment, _is_new) = issue::find_or_make_bot_comment(&ctx, issue)
|
||||
.await
|
||||
.with_context(|| format!("Failed to find or make bot comment in issue #{issue}"))?;
|
||||
|
||||
if bot_comment.body.contains(&hash) {
|
||||
log::info!("Issue #{issue} is up-to-date, not editing comment.");
|
||||
issue::remove_stale_label(&ctx, issue)
|
||||
.await
|
||||
.with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?;
|
||||
|
||||
continue 'issues;
|
||||
}
|
||||
|
||||
log::info!("Updating bot comment in issue #{issue} ...");
|
||||
issue::update_bot_comment_from_subtree(&ctx, bot_comment.id, &subtree, &hash)
|
||||
.await
|
||||
.with_context(|| format!("Failed to update bot comment in issue #{issue}"))?;
|
||||
|
||||
issue::remove_stale_label(&ctx, issue)
|
||||
.await
|
||||
.with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
run().await
|
||||
}
|
112
techtree-manager/src/render.rs
Normal file
112
techtree-manager/src/render.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::Context as _;
|
||||
|
||||
trait SuccessExt {
|
||||
fn success(&mut self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl SuccessExt for Command {
|
||||
fn success(&mut self) -> anyhow::Result<()> {
|
||||
if !self.status()?.success() {
|
||||
anyhow::bail!("Command returned unsuccessfully");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderedTree {
|
||||
repo_dir: std::path::PathBuf,
|
||||
dot_file: std::path::PathBuf,
|
||||
svg_file: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub async fn render(
|
||||
_ctx: &crate::Context,
|
||||
tree: &crate::tree::Tree,
|
||||
) -> anyhow::Result<RenderedTree> {
|
||||
let repo_dir = std::path::PathBuf::from("render-git");
|
||||
|
||||
if repo_dir.is_dir() {
|
||||
log::info!("Found old {repo_dir:?} repository, removing...");
|
||||
std::fs::remove_dir_all(&repo_dir).context("Failed to remove stale render repository")?;
|
||||
}
|
||||
|
||||
std::fs::create_dir(&repo_dir).context("Failed creating directory for rendered graph")?;
|
||||
|
||||
let dot_file = repo_dir.join("techtree.dot");
|
||||
let svg_file = repo_dir.join("techtree.svg");
|
||||
|
||||
let dot_source = tree.to_dot();
|
||||
std::fs::write(&dot_file, dot_source.as_bytes())
|
||||
.context("Failed to write `dot` source file")?;
|
||||
|
||||
Command::new("dot")
|
||||
.args(["-T", "svg"])
|
||||
.arg("-o")
|
||||
.arg(&svg_file)
|
||||
.arg(&dot_file)
|
||||
.success()
|
||||
.context("Failed to generate svg graph from dot source")?;
|
||||
|
||||
Ok(RenderedTree {
|
||||
repo_dir,
|
||||
dot_file,
|
||||
svg_file,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::Result<()> {
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.arg("init")
|
||||
.arg("--initial-branch=render")
|
||||
.arg("--quiet")
|
||||
.success()
|
||||
.context("Failed to initialize render repository")?;
|
||||
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.args(["config", "user.email", "git@fa-fo.de"])
|
||||
.success()
|
||||
.context("Failed to configure identity for render repo")?;
|
||||
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.args(["config", "user.name", "Forgejo Actions"])
|
||||
.success()
|
||||
.context("Failed to configure identity for render repo")?;
|
||||
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.arg("add")
|
||||
.arg(rendered.dot_file.strip_prefix(&rendered.repo_dir).unwrap())
|
||||
.arg(rendered.svg_file.strip_prefix(&rendered.repo_dir).unwrap())
|
||||
.success()
|
||||
.context("Failed to add generated graph files to git index")?;
|
||||
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.arg("commit")
|
||||
.args(["-m", &format!("Updated techtree at {}", ctx.timestamp)])
|
||||
.success()
|
||||
.context("Failed to add generated graph files to git index")?;
|
||||
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&rendered.repo_dir)
|
||||
.arg("push")
|
||||
.arg("--force")
|
||||
.arg("--quiet")
|
||||
.arg(ctx.repo_auth_url.to_string())
|
||||
.arg("HEAD:refs/heads/render")
|
||||
.status()
|
||||
.context("Failed to push rendered graph to forgejo repository")?;
|
||||
|
||||
Ok(())
|
||||
}
|
353
techtree-manager/src/tree.rs
Normal file
353
techtree-manager/src/tree.rs
Normal file
|
@ -0,0 +1,353 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use petgraph::visit::EdgeRef as _;
|
||||
|
||||
/// Element in the techtree
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct Element {
|
||||
/// Issue associated with this element
|
||||
pub issue_number: u64,
|
||||
/// Description of this element
|
||||
pub description: String,
|
||||
/// Type of this element
|
||||
pub ty: String,
|
||||
/// Completion status of this element.
|
||||
pub status: ElementStatus,
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn to_dot_node_attributes(&self, role: Option<SubtreeElementRole>) -> String {
|
||||
let Element {
|
||||
issue_number,
|
||||
description,
|
||||
ty,
|
||||
status,
|
||||
} = self;
|
||||
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
attributes.push(format!(
|
||||
r##"label = <{{{{#{issue_number} | {status}}}|<I>{ty}</I>|<B>{description}</B>}}>"##
|
||||
));
|
||||
attributes.push(r#"shape = "record""#.to_owned());
|
||||
|
||||
let (color, background) = match (role, status) {
|
||||
(Some(SubtreeElementRole::ElementOfInterest), _) => ("black", "white"),
|
||||
(Some(SubtreeElementRole::Dependant), _) => ("gray", "gray"),
|
||||
(_, ElementStatus::Missing) => ("#800", "#fcc"),
|
||||
(_, ElementStatus::Assigned) => ("#a50", "#ffa"),
|
||||
(_, ElementStatus::Completed) => ("#080", "#afa"),
|
||||
};
|
||||
attributes.push(format!(r#"color = "{color}""#));
|
||||
attributes.push(format!(r#"fontcolor = "{color}""#));
|
||||
attributes.push(format!(r#"fillcolor = "{background}""#));
|
||||
attributes.push(format!(r#"style = "filled""#));
|
||||
|
||||
attributes.join(", ")
|
||||
}
|
||||
|
||||
fn to_mermaid_node(
|
||||
&self,
|
||||
index: ElementIndex,
|
||||
role: Option<SubtreeElementRole>,
|
||||
repo_url: &str,
|
||||
simple: bool,
|
||||
) -> String {
|
||||
let Element {
|
||||
issue_number,
|
||||
description,
|
||||
ty,
|
||||
status,
|
||||
} = self;
|
||||
|
||||
let label = if simple {
|
||||
format!(r##"#{issue_number} | {status}\n<i>{ty}</i>\n<b>{description}</b>"##)
|
||||
} else {
|
||||
format!(
|
||||
r##"<a href='{repo_url}/issues/{issue_number}' target='_blank'>#{issue_number}</a> | {status}\n<i>{ty}</i>\n<b>{description}</b>"##
|
||||
)
|
||||
};
|
||||
|
||||
let class = match (role, status) {
|
||||
(Some(SubtreeElementRole::ElementOfInterest), _) => "eoi",
|
||||
(Some(SubtreeElementRole::Dependant), _) => "dependant",
|
||||
(_, ElementStatus::Missing) => "dep_missing",
|
||||
(_, ElementStatus::Assigned) => "dep_assigned",
|
||||
(_, ElementStatus::Completed) => "dep_completed",
|
||||
};
|
||||
|
||||
format!(
|
||||
" {index}:::{class}\n {index}[\"{label}\"]",
|
||||
index = index.index(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn mermaid_classes() -> String {
|
||||
r##"
|
||||
classDef eoi fill:#fff, stroke:#000, color:#000;
|
||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
||||
classDef dep_missing fill:#fcc, stroke:#800, color:#000;
|
||||
classDef dep_assigned fill:#ffa, stroke:#a50, color:#000;
|
||||
classDef dep_completed fill:#afa, stroke:#080, color:#000;
|
||||
"##
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum ElementStatus {
|
||||
Missing,
|
||||
Assigned,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ElementStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
ElementStatus::Missing => "MISSING",
|
||||
ElementStatus::Assigned => "ASSIGNED",
|
||||
ElementStatus::Completed => "COMPLETED",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type ElementIndex = petgraph::graph::NodeIndex;
|
||||
|
||||
pub struct Tree {
|
||||
graph: petgraph::Graph<Element, ()>,
|
||||
issue_map: BTreeMap<u64, ElementIndex>,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graph: petgraph::Graph::new(),
|
||||
issue_map: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_element(&mut self, element: Element) {
|
||||
let issue_number = element.issue_number;
|
||||
assert!(!self.issue_map.contains_key(&issue_number));
|
||||
let idx = self.graph.add_node(element);
|
||||
self.issue_map.insert(issue_number, idx);
|
||||
}
|
||||
|
||||
pub fn add_dependency_by_issue_number(&mut self, dependant: u64, dependency: u64) {
|
||||
let a = self.find_element_by_issue_number(dependant).unwrap();
|
||||
let b = self.find_element_by_issue_number(dependency).unwrap();
|
||||
self.graph.add_edge(a, b, ());
|
||||
}
|
||||
|
||||
pub fn find_element_by_issue_number(&self, issue_number: u64) -> Option<ElementIndex> {
|
||||
self.issue_map.get(&issue_number).copied()
|
||||
}
|
||||
|
||||
pub fn subtree_for_issue<'a>(&'a self, issue_number: u64) -> Option<Subtree<'a>> {
|
||||
Some(Subtree::new_for_element(
|
||||
self,
|
||||
self.find_element_by_issue_number(issue_number)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn iter_issues<'a>(&'a self) -> impl Iterator<Item = u64> + 'a {
|
||||
self.issue_map.keys().copied()
|
||||
}
|
||||
|
||||
pub fn subtree_for_element<'a>(&'a self, element: ElementIndex) -> Subtree<'a> {
|
||||
Subtree::new_for_element(self, element)
|
||||
}
|
||||
|
||||
pub fn to_dot(&self) -> String {
|
||||
let dot = petgraph::dot::Dot::with_attr_getters(
|
||||
&self.graph,
|
||||
&[
|
||||
petgraph::dot::Config::EdgeNoLabel,
|
||||
petgraph::dot::Config::NodeNoLabel,
|
||||
petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT),
|
||||
petgraph::dot::Config::GraphContentOnly,
|
||||
],
|
||||
&|_g, _edge_id| "".to_string(),
|
||||
&|_g, (_, element)| element.to_dot_node_attributes(None),
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"digraph {{
|
||||
ranksep=1.2
|
||||
{:?}
|
||||
}}
|
||||
"#,
|
||||
dot
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_mermaid(&self, repo_url: &str, simple: bool) -> String {
|
||||
let mut mermaid = String::new();
|
||||
mermaid.push_str("flowchart BT\n");
|
||||
mermaid.push_str(&mermaid_classes());
|
||||
|
||||
for index in self.graph.node_indices() {
|
||||
mermaid.push_str(&self.graph[index].to_mermaid_node(index, None, repo_url, simple));
|
||||
mermaid.push_str("\n");
|
||||
}
|
||||
|
||||
for edge in self.graph.edge_references() {
|
||||
mermaid.push_str(&format!(
|
||||
" {} --> {}\n",
|
||||
edge.source().index(),
|
||||
edge.target().index()
|
||||
));
|
||||
}
|
||||
|
||||
mermaid
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct SubtreeElement<'a> {
|
||||
element: &'a Element,
|
||||
role: SubtreeElementRole,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum SubtreeElementRole {
|
||||
ElementOfInterest,
|
||||
Dependency,
|
||||
Dependant,
|
||||
}
|
||||
|
||||
pub struct Subtree<'a> {
|
||||
original: &'a Tree,
|
||||
graph: petgraph::Graph<SubtreeElement<'a>, ()>,
|
||||
index_map: BTreeMap<ElementIndex, ElementIndex>,
|
||||
}
|
||||
|
||||
impl<'a> Subtree<'a> {
|
||||
pub fn new_for_element(tree: &'a Tree, element: ElementIndex) -> Subtree<'a> {
|
||||
let mut this = Self {
|
||||
original: tree,
|
||||
graph: petgraph::Graph::new(),
|
||||
index_map: BTreeMap::new(),
|
||||
};
|
||||
let graph = &this.original.graph;
|
||||
|
||||
this.add_node_if_missing(element, SubtreeElementRole::ElementOfInterest);
|
||||
|
||||
let mut dfs = petgraph::visit::Dfs::new(&graph, element);
|
||||
while let Some(idx) = dfs.next(&graph) {
|
||||
this.add_node_if_missing(idx, SubtreeElementRole::Dependency);
|
||||
}
|
||||
|
||||
for idx in this.index_map.keys().copied() {
|
||||
for neighbor in graph.neighbors_directed(idx, petgraph::Direction::Outgoing) {
|
||||
if !this.index_map.contains_key(&neighbor) {
|
||||
log::warn!(
|
||||
"Probably missed a dependency from #{from} to #{to} while generating subtree for #{el}",
|
||||
el = graph[element].issue_number,
|
||||
from = graph[idx].issue_number,
|
||||
to = graph[neighbor].issue_number
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.graph
|
||||
.add_edge(this.index_map[&idx], this.index_map[&neighbor], ());
|
||||
}
|
||||
}
|
||||
|
||||
for neighbor in graph.neighbors_directed(element, petgraph::Direction::Incoming) {
|
||||
if this.index_map.contains_key(&neighbor) {
|
||||
log::warn!(
|
||||
"Already found #{from} in dependencies of #{to}, but it should have been the other way around?!",
|
||||
from = graph[neighbor].issue_number,
|
||||
to = graph[element].issue_number
|
||||
);
|
||||
} else {
|
||||
this.add_node_if_missing(neighbor, SubtreeElementRole::Dependant);
|
||||
}
|
||||
|
||||
this.graph
|
||||
.add_edge(this.index_map[&neighbor], this.index_map[&element], ());
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn add_node_if_missing(&mut self, index: ElementIndex, role: SubtreeElementRole) -> bool {
|
||||
if self.index_map.contains_key(&index) {
|
||||
debug_assert!(self.graph.node_weight(self.index_map[&index]).is_some());
|
||||
return false;
|
||||
}
|
||||
|
||||
let subtree_element = SubtreeElement {
|
||||
element: &self.original.graph[index],
|
||||
role,
|
||||
};
|
||||
|
||||
let new_idx = self.graph.add_node(subtree_element);
|
||||
self.index_map.insert(index, new_idx);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn to_dot(&self) -> String {
|
||||
let dot = petgraph::dot::Dot::with_attr_getters(
|
||||
&self.graph,
|
||||
&[
|
||||
petgraph::dot::Config::EdgeNoLabel,
|
||||
petgraph::dot::Config::NodeNoLabel,
|
||||
petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT),
|
||||
],
|
||||
&|_g, _edge_id| "".to_string(),
|
||||
&|_g, (_, element)| element.element.to_dot_node_attributes(Some(element.role)),
|
||||
);
|
||||
|
||||
format!("{:?}", dot)
|
||||
}
|
||||
|
||||
pub fn to_mermaid(&self, repo_url: &str, simple: bool) -> String {
|
||||
let mut mermaid = String::new();
|
||||
mermaid.push_str("flowchart BT\n");
|
||||
mermaid.push_str(&mermaid_classes());
|
||||
|
||||
for index in self.graph.node_indices() {
|
||||
let node = &self.graph[index];
|
||||
mermaid.push_str(&node.element.to_mermaid_node(
|
||||
index,
|
||||
Some(node.role),
|
||||
repo_url,
|
||||
simple,
|
||||
));
|
||||
mermaid.push_str("\n");
|
||||
}
|
||||
|
||||
for edge in self.graph.edge_references() {
|
||||
mermaid.push_str(&format!(
|
||||
" {} --> {}\n",
|
||||
edge.source().index(),
|
||||
edge.target().index()
|
||||
));
|
||||
}
|
||||
|
||||
mermaid
|
||||
}
|
||||
|
||||
pub fn stable_hash(&self) -> String {
|
||||
let mut nodes: Vec<_> = self.graph.node_weights().collect();
|
||||
nodes.sort_by_key(|n| n.element.issue_number);
|
||||
let mut edges: Vec<_> = self
|
||||
.graph
|
||||
.edge_references()
|
||||
.map(|edge| {
|
||||
(
|
||||
self.graph[edge.source()].element.issue_number,
|
||||
self.graph[edge.target()].element.issue_number,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
edges.sort();
|
||||
|
||||
let json_data = serde_json::ser::to_string(&(nodes, edges)).unwrap();
|
||||
sha256::digest(json_data)
|
||||
}
|
||||
}
|
40
techtree-manager/src/wiki.rs
Normal file
40
techtree-manager/src/wiki.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use anyhow::Context as _;
|
||||
use base64::prelude::*;
|
||||
|
||||
pub async fn update_wiki_from_tree(
|
||||
ctx: &crate::Context,
|
||||
tree: &crate::tree::Tree,
|
||||
) -> anyhow::Result<()> {
|
||||
let mermaid = tree.to_mermaid(&ctx.repo_url.to_string(), false);
|
||||
let wiki_text = format!(
|
||||
r##"This page is automatically updated to show the latest and greatest FAFO techtree:
|
||||
|
||||
```mermaid
|
||||
{mermaid}
|
||||
```
|
||||
"##
|
||||
);
|
||||
update_wiki_overview(&ctx, wiki_text)
|
||||
.await
|
||||
.context("Failed to update the techtree wiki page")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_wiki_overview(ctx: &crate::Context, new_body: String) -> anyhow::Result<()> {
|
||||
// TODO: Figure out why we get a 404 when the edit was successfull...
|
||||
let _ = ctx
|
||||
.forgejo
|
||||
.repo_edit_wiki_page(
|
||||
&ctx.owner,
|
||||
&ctx.repo,
|
||||
"Home",
|
||||
forgejo_api::structs::CreateWikiPageOptions {
|
||||
content_base64: Some(BASE64_STANDARD.encode(new_body.as_bytes())),
|
||||
message: Some(format!("Updated to latest model at {}", ctx.timestamp)),
|
||||
title: Some("Home".to_owned()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed editing the wiki page");
|
||||
Ok(())
|
||||
}
|
184
techtree.dot
184
techtree.dot
|
@ -1,184 +0,0 @@
|
|||
digraph {
|
||||
ranksep=1.2
|
||||
rankdir="BT"
|
||||
0 [ label = <{{#76 | ASSIGNED}|<I>Process</I>|<B>Video Streaming</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
1 [ label = <{{#75 | ASSIGNED}|<I>Equipment</I>|<B>Microphone(s)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
2 [ label = <{{#74 | ASSIGNED}|<I>Equipment</I>|<B>Camcorder</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
3 [ label = <{{#73 | ASSIGNED}|<I>Development</I>|<B>Main Camera Rig</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
4 [ label = <{{#72 | MISSING}|<I>Process</I>|<B>STM Non-metallic imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
5 [ label = <{{#71 | MISSING}|<I>Research</I>|<B>STM Metal Pattern Deposition</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
6 [ label = <{{#70 | MISSING}|<I>Research</I>|<B>STM Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
7 [ label = <{{#69 | MISSING}|<I>Research</I>|<B>STM Surface Topography Manipulation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
8 [ label = <{{#68 | ASSIGNED}|<I>Equipment</I>|<B>Heating Stirrer</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
9 [ label = <{{#67 | MISSING}|<I>Equipment</I>|<B>Chemical Storage/Containment</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
10 [ label = <{{#66 | ASSIGNED}|<I>Process</I>|<B>Video Recording</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
11 [ label = <{{#65 | MISSING}|<I>Process</I>|<B>Gas chromatography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
12 [ label = <{{#64 | COMPLETED}|<I>Equipment</I>|<B>FFF 3D-Printer</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
13 [ label = <{{#63 | MISSING}|<I>Equipment</I>|<B>OpenFlexure Delta Stage</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
14 [ label = <{{#62 | MISSING}|<I>Process</I>|<B>Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
15 [ label = <{{#61 | MISSING}|<I>Process</I>|<B>Zeloof Z1 process</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
16 [ label = <{{#60 | MISSING}|<I>Process</I>|<B>Thermal diffusion doping</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
17 [ label = <{{#59 | MISSING}|<I>Process</I>|<B>H₃PO₄/HNO₃/AcOH Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
18 [ label = <{{#58 | MISSING}|<I>Process</I>|<B>Pattern Al</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
19 [ label = <{{#57 | MISSING}|<I>Process</I>|<B>Metal thin film deposition</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
20 [ label = <{{#56 | MISSING}|<I>Process</I>|<B>PVD: Thermal Evaporation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
21 [ label = <{{#55 | MISSING}|<I>Equipment</I>|<B>Tube Furnace</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
22 [ label = <{{#54 | MISSING}|<I>Process</I>|<B>Si Thermal Oxidation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
23 [ label = <{{#53 | MISSING}|<I>Process</I>|<B>Pattern SiO₂</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
24 [ label = <{{#52 | MISSING}|<I>Development</I>|<B>Maskless photolithography stepper</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
25 [ label = <{{#51 | MISSING}|<I>Process</I>|<B>Maskless Photolithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
26 [ label = <{{#50 | MISSING}|<I>Process</I>|<B>E-beam Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
27 [ label = <{{#49 | MISSING}|<I>Development</I>|<B>Spin Coater</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
28 [ label = <{{#48 | MISSING}|<I>Process</I>|<B>STM Imaging Individual Atoms</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
29 [ label = <{{#47 | MISSING}|<I>Process</I>|<B>STM Imaging@1nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
30 [ label = <{{#46 | MISSING}|<I>Process</I>|<B>STM Imaging@10nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
31 [ label = <{{#45 | MISSING}|<I>Process</I>|<B>STM Imaging@100nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
32 [ label = <{{#44 | MISSING}|<I>Process</I>|<B>STM Imaging@1µm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
33 [ label = <{{#43 | ASSIGNED}|<I>Development</I>|<B>Scanning Tunneling Microscope (STM)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
34 [ label = <{{#42 | ASSIGNED}|<I>Development</I>|<B>STM Vibration Isolation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
35 [ label = <{{#41 | COMPLETED}|<I>Process</I>|<B>Primitive Die Imaging</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
36 [ label = <{{#40 | ASSIGNED}|<I>Equipment</I>|<B>Air Compressor</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
37 [ label = <{{#39 | MISSING}|<I>Development</I>|<B>OBI Lite</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
38 [ label = <{{#38 | MISSING}|<I>Process</I>|<B>SEM Non-Metallic Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
39 [ label = <{{#37 | MISSING}|<I>Process</I>|<B>PVD: Sputtering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
40 [ label = <{{#36 | MISSING}|<I>Process</I>|<B>Primitive Chip Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
41 [ label = <{{#35 | MISSING}|<I>Equipment</I>|<B>SEM Beam Blanker</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
42 [ label = <{{#34 | COMPLETED}|<I>Equipment</I>|<B>Open Beam Interface</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
43 [ label = <{{#33 | ASSIGNED}|<I>Equipment</I>|<B>SEM Vibration Isolation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
44 [ label = <{{#32 | MISSING}|<I>Process</I>|<B>SEM Imaging@10nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
45 [ label = <{{#31 | MISSING}|<I>Process</I>|<B>SEM Imaging@100nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
46 [ label = <{{#30 | MISSING}|<I>Process</I>|<B>SEM Imaging@1µm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
47 [ label = <{{#29 | COMPLETED}|<I>Process</I>|<B>SEM Imaging@10µm</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
48 [ label = <{{#28 | COMPLETED}|<I>Equipment</I>|<B>Scanning Electron Microscope (SEM)</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
49 [ label = <{{#27 | ASSIGNED}|<I>Equipment</I>|<B>Technical Ventilation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
50 [ label = <{{#26 | COMPLETED}|<I>Equipment</I>|<B>Water Cooling</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||
51 [ label = <{{#25 | MISSING}|<I>Equipment</I>|<B>Plasma Etcher</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
52 [ label = <{{#24 | ASSIGNED}|<I>Equipment</I>|<B>~3ph Power (below 10kW)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
53 [ label = <{{#23 | MISSING}|<I>Equipment</I>|<B>Fume Hood</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
54 [ label = <{{#22 | MISSING}|<I>Equipment</I>|<B>Running Water and Sink</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
55 [ label = <{{#21 | MISSING}|<I>Process</I>|<B>Reactive Ion Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
56 [ label = <{{#20 | MISSING}|<I>Process</I>|<B>Body Bias Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
57 [ label = <{{#19 | MISSING}|<I>Process</I>|<B>IC Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
58 [ label = <{{#18 | MISSING}|<I>Research</I>|<B>SEM Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
59 [ label = <{{#17 | MISSING}|<I>Process</I>|<B>SEM Nanoprobing/Live Analysis</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
60 [ label = <{{#16 | MISSING}|<I>Process</I>|<B>Laser Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
61 [ label = <{{#15 | MISSING}|<I>Process</I>|<B>Microprobing</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
62 [ label = <{{#14 | MISSING}|<I>Equipment</I>|<B>Optical Microscope</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
63 [ label = <{{#13 | MISSING}|<I>Process</I>|<B>Full Chip Reverse Engineering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
64 [ label = <{{#12 | MISSING}|<I>Process</I>|<B>DASH Stain</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
65 [ label = <{{#11 | MISSING}|<I>Process</I>|<B>Simple Chip Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
66 [ label = <{{#10 | ASSIGNED}|<I>Equipment</I>|<B>SEM: Motorized Stage</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
67 [ label = <{{#9 | MISSING}|<I>Equipment</I>|<B>Optical Microscope: Motorized Stage</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
68 [ label = <{{#8 | MISSING}|<I>Process</I>|<B>Plasma Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
69 [ label = <{{#7 | MISSING}|<I>Equipment</I>|<B>Fiber Laser</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
70 [ label = <{{#6 | MISSING}|<I>Process</I>|<B>HNO₃/H₂SO₄ Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
71 [ label = <{{#5 | MISSING}|<I>Process</I>|<B>HNO₃/HF Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
72 [ label = <{{#4 | ASSIGNED}|<I>Equipment</I>|<B>Wet Lab (Chemistry)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||
73 [ label = <{{#3 | MISSING}|<I>Equipment</I>|<B>Lapping Machine</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
74 [ label = <{{#2 | MISSING}|<I>Process</I>|<B>Chip Delayering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
75 [ label = <{{#1 | MISSING}|<I>Process</I>|<B>Chip Decapping</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||
0 -> 3 [ ]
|
||||
3 -> 1 [ ]
|
||||
3 -> 2 [ ]
|
||||
4 -> 19 [ ]
|
||||
4 -> 33 [ ]
|
||||
5 -> 33 [ ]
|
||||
6 -> 27 [ ]
|
||||
6 -> 33 [ ]
|
||||
7 -> 29 [ ]
|
||||
9 -> 49 [ ]
|
||||
10 -> 3 [ ]
|
||||
11 -> 72 [ ]
|
||||
13 -> 12 [ ]
|
||||
14 -> 6 [ ]
|
||||
14 -> 25 [ ]
|
||||
14 -> 26 [ ]
|
||||
15 -> 16 [ ]
|
||||
15 -> 18 [ ]
|
||||
15 -> 23 [ ]
|
||||
15 -> 61 [ ]
|
||||
16 -> 21 [ ]
|
||||
16 -> 72 [ ]
|
||||
17 -> 72 [ ]
|
||||
18 -> 5 [ ]
|
||||
18 -> 14 [ ]
|
||||
18 -> 17 [ ]
|
||||
18 -> 19 [ ]
|
||||
19 -> 20 [ ]
|
||||
19 -> 39 [ ]
|
||||
22 -> 21 [ ]
|
||||
23 -> 14 [ ]
|
||||
23 -> 22 [ ]
|
||||
23 -> 55 [ ]
|
||||
23 -> 71 [ ]
|
||||
25 -> 24 [ ]
|
||||
25 -> 27 [ ]
|
||||
26 -> 27 [ ]
|
||||
26 -> 41 [ ]
|
||||
28 -> 29 [ ]
|
||||
29 -> 30 [ ]
|
||||
30 -> 31 [ ]
|
||||
31 -> 32 [ ]
|
||||
32 -> 33 [ ]
|
||||
33 -> 34 [ ]
|
||||
35 -> 47 [ ]
|
||||
37 -> 42 [ ]
|
||||
38 -> 19 [ ]
|
||||
38 -> 48 [ ]
|
||||
40 -> 35 [ ]
|
||||
40 -> 75 [ ]
|
||||
41 -> 48 [ ]
|
||||
42 -> 48 [ ]
|
||||
43 -> 36 [ ]
|
||||
43 -> 48 [ ]
|
||||
44 -> 45 [ ]
|
||||
45 -> 43 [ ]
|
||||
45 -> 46 [ ]
|
||||
46 -> 47 [ ]
|
||||
47 -> 48 [ ]
|
||||
48 -> 50 [ ]
|
||||
51 -> 49 [ ]
|
||||
51 -> 52 [ ]
|
||||
53 -> 49 [ ]
|
||||
55 -> 51 [ ]
|
||||
56 -> 75 [ ]
|
||||
57 -> 56 [ ]
|
||||
57 -> 58 [ ]
|
||||
57 -> 60 [ ]
|
||||
57 -> 61 [ ]
|
||||
58 -> 41 [ ]
|
||||
58 -> 59 [ ]
|
||||
59 -> 66 [ ]
|
||||
59 -> 74 [ ]
|
||||
60 -> 67 [ ]
|
||||
60 -> 75 [ ]
|
||||
61 -> 67 [ ]
|
||||
63 -> 64 [ ]
|
||||
63 -> 65 [ ]
|
||||
64 -> 72 [ ]
|
||||
65 -> 40 [ ]
|
||||
65 -> 45 [ ]
|
||||
65 -> 66 [ ]
|
||||
65 -> 74 [ ]
|
||||
66 -> 48 [ ]
|
||||
67 -> 13 [ ]
|
||||
67 -> 62 [ ]
|
||||
68 -> 51 [ ]
|
||||
70 -> 72 [ ]
|
||||
71 -> 72 [ ]
|
||||
72 -> 8 [ ]
|
||||
72 -> 9 [ ]
|
||||
72 -> 49 [ ]
|
||||
72 -> 53 [ ]
|
||||
72 -> 54 [ ]
|
||||
74 -> 55 [ ]
|
||||
74 -> 69 [ ]
|
||||
74 -> 71 [ ]
|
||||
74 -> 73 [ ]
|
||||
74 -> 75 [ ]
|
||||
75 -> 68 [ ]
|
||||
75 -> 69 [ ]
|
||||
75 -> 70 [ ]
|
||||
75 -> 73 [ ]
|
||||
|
||||
}
|
1542
techtree.svg
1542
techtree.svg
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 103 KiB |
Loading…
Reference in a new issue